bc-api-client 0.2.1 → 1.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/README.md +66 -205
- package/dist/index.d.mts +1013 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1424 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +31 -26
- package/dist/auth.d.ts +0 -134
- package/dist/client.d.ts +0 -160
- package/dist/core.d.ts +0 -27
- package/dist/endpoints.d.ts +0 -471
- package/dist/endpoints.js +0 -486
- package/dist/endpoints.js.map +0 -7
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -741
- package/dist/index.js.map +0 -7
- package/dist/net.d.ts +0 -71
- package/dist/util.d.ts +0 -22
package/dist/index.js
DELETED
|
@@ -1,741 +0,0 @@
|
|
|
1
|
-
// src/net.ts
|
|
2
|
-
import ky, { HTTPError } from "ky";
|
|
3
|
-
var Methods = {
|
|
4
|
-
GET: "GET",
|
|
5
|
-
POST: "POST",
|
|
6
|
-
PUT: "PUT",
|
|
7
|
-
DELETE: "DELETE"
|
|
8
|
-
};
|
|
9
|
-
var BASE_URL = "https://api.bigcommerce.com/stores/";
|
|
10
|
-
var CONFIG = {
|
|
11
|
-
/** Base URL for BigCommerce API */
|
|
12
|
-
BASE_URL,
|
|
13
|
-
/** Default API version to use */
|
|
14
|
-
DEFAULT_VERSION: "v3",
|
|
15
|
-
/** Maximum delay in milliseconds for rate limit retries */
|
|
16
|
-
DEFAULT_MAX_DELAY: 6e4,
|
|
17
|
-
/** Maximum allowed URL length */
|
|
18
|
-
MAX_URL_LENGTH: 2048,
|
|
19
|
-
/** Default maximum number of retries for rate-limited requests */
|
|
20
|
-
DEFAULT_MAX_RETRIES: 5,
|
|
21
|
-
/** Rate limit header names */
|
|
22
|
-
HEADERS: {
|
|
23
|
-
/** Time window for rate limiting in milliseconds */
|
|
24
|
-
WINDOW: "x-rate-limit-time-window-ms",
|
|
25
|
-
/** Time to wait before retrying after rate limit in milliseconds */
|
|
26
|
-
RETRY_AFTER: "x-rate-limit-time-reset-ms",
|
|
27
|
-
/** Total request quota for the time window */
|
|
28
|
-
REQUEST_QUOTA: "x-rate-limit-requests-quota",
|
|
29
|
-
/** Number of requests remaining in the current window */
|
|
30
|
-
REQUESTS_LEFT: "x-rate-limit-requests-left"
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
var RequestError = class extends Error {
|
|
34
|
-
constructor(status, message, data, cause) {
|
|
35
|
-
super(message, { cause });
|
|
36
|
-
this.status = status;
|
|
37
|
-
this.message = message;
|
|
38
|
-
this.data = data;
|
|
39
|
-
this.cause = cause;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
var request = async (options) => {
|
|
43
|
-
const {
|
|
44
|
-
baseUrl = CONFIG.BASE_URL,
|
|
45
|
-
maxDelay = CONFIG.DEFAULT_MAX_DELAY,
|
|
46
|
-
maxRetries = CONFIG.DEFAULT_MAX_RETRIES,
|
|
47
|
-
logger
|
|
48
|
-
} = options;
|
|
49
|
-
let retries = 0;
|
|
50
|
-
let lastError = null;
|
|
51
|
-
while (retries < maxRetries) {
|
|
52
|
-
try {
|
|
53
|
-
return await safeRequest({ ...options, baseUrl });
|
|
54
|
-
} catch (error) {
|
|
55
|
-
const err = error;
|
|
56
|
-
lastError = err;
|
|
57
|
-
if (err.status === 429 && typeof err.data === "object" && err.data !== null && "headers" in err.data) {
|
|
58
|
-
const headers = err.data.headers;
|
|
59
|
-
const retryAfter = Number.parseInt(headers[CONFIG.HEADERS.RETRY_AFTER]);
|
|
60
|
-
logger?.debug(
|
|
61
|
-
{
|
|
62
|
-
retryAfter,
|
|
63
|
-
retries,
|
|
64
|
-
remaining: headers[CONFIG.HEADERS.REQUESTS_LEFT]
|
|
65
|
-
},
|
|
66
|
-
"Rate limit hit, retrying"
|
|
67
|
-
);
|
|
68
|
-
if (Number.isNaN(retryAfter)) {
|
|
69
|
-
throw new RequestError(
|
|
70
|
-
err.status,
|
|
71
|
-
`Failed to parse retry after: ${headers[CONFIG.HEADERS.RETRY_AFTER]}, ${err.message}`,
|
|
72
|
-
err.data,
|
|
73
|
-
err.cause
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
if (retryAfter > maxDelay) {
|
|
77
|
-
logger?.warn(
|
|
78
|
-
{
|
|
79
|
-
retryAfter,
|
|
80
|
-
maxDelay
|
|
81
|
-
},
|
|
82
|
-
"Rate limit delay exceeds maximum allowed delay"
|
|
83
|
-
);
|
|
84
|
-
throw new RequestError(
|
|
85
|
-
err.status,
|
|
86
|
-
`Rate limit exceeded: ${retryAfter}ms, ${err.message}`,
|
|
87
|
-
err.data,
|
|
88
|
-
err.cause
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
await new Promise((resolve) => setTimeout(resolve, retryAfter));
|
|
92
|
-
retries++;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
logger?.error(
|
|
99
|
-
{
|
|
100
|
-
retries,
|
|
101
|
-
error: lastError
|
|
102
|
-
},
|
|
103
|
-
"Request failed after maximum retries"
|
|
104
|
-
);
|
|
105
|
-
throw lastError ?? new RequestError(500, "Failed to make request", "Too many retries after rate limit");
|
|
106
|
-
};
|
|
107
|
-
var safeRequest = async (options) => {
|
|
108
|
-
const { logger, baseUrl = CONFIG.BASE_URL } = options;
|
|
109
|
-
let res;
|
|
110
|
-
try {
|
|
111
|
-
res = await call({ ...options, baseUrl });
|
|
112
|
-
} catch (error) {
|
|
113
|
-
if (error instanceof RequestError) {
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
if (!(error instanceof HTTPError)) {
|
|
117
|
-
logger?.error(
|
|
118
|
-
{
|
|
119
|
-
error: error instanceof Error ? {
|
|
120
|
-
name: error.name,
|
|
121
|
-
message: error.message
|
|
122
|
-
} : error
|
|
123
|
-
},
|
|
124
|
-
"Unexpected error during request"
|
|
125
|
-
);
|
|
126
|
-
throw error;
|
|
127
|
-
}
|
|
128
|
-
let data;
|
|
129
|
-
let errorMessage = error.message;
|
|
130
|
-
try {
|
|
131
|
-
data = await error.response.text();
|
|
132
|
-
try {
|
|
133
|
-
data = JSON.parse(data);
|
|
134
|
-
if (typeof data === "object" && data !== null && "message" in data) {
|
|
135
|
-
errorMessage = data.message;
|
|
136
|
-
}
|
|
137
|
-
} catch {
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
data = "Failed to read error response";
|
|
141
|
-
}
|
|
142
|
-
logger?.error(
|
|
143
|
-
{
|
|
144
|
-
status: error?.response?.status,
|
|
145
|
-
errorMessage,
|
|
146
|
-
data,
|
|
147
|
-
endpoint: options.endpoint,
|
|
148
|
-
query: options.query,
|
|
149
|
-
body: options.body,
|
|
150
|
-
headers: Object.fromEntries(error?.response?.headers?.entries() ?? [])
|
|
151
|
-
},
|
|
152
|
-
"HTTP error during request"
|
|
153
|
-
);
|
|
154
|
-
throw new RequestError(
|
|
155
|
-
error?.response?.status ?? 500,
|
|
156
|
-
errorMessage,
|
|
157
|
-
{
|
|
158
|
-
data,
|
|
159
|
-
endpoint: options.endpoint,
|
|
160
|
-
query: options.query,
|
|
161
|
-
body: options.body,
|
|
162
|
-
headers: Object.fromEntries(error?.response?.headers?.entries() ?? [])
|
|
163
|
-
},
|
|
164
|
-
error
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
const text = await res.text();
|
|
168
|
-
if (res.status === 204) {
|
|
169
|
-
return void 0;
|
|
170
|
-
}
|
|
171
|
-
try {
|
|
172
|
-
return JSON.parse(text);
|
|
173
|
-
} catch (error) {
|
|
174
|
-
logger?.error(
|
|
175
|
-
{
|
|
176
|
-
status: res.status,
|
|
177
|
-
error: error instanceof Error ? {
|
|
178
|
-
name: error.name,
|
|
179
|
-
message: error.message
|
|
180
|
-
} : error
|
|
181
|
-
},
|
|
182
|
-
"Failed to parse response"
|
|
183
|
-
);
|
|
184
|
-
throw new RequestError(res.status, `Failed to parse response: ${text}`, text, error);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
var call = async (options) => {
|
|
188
|
-
const {
|
|
189
|
-
storeHash,
|
|
190
|
-
accessToken,
|
|
191
|
-
endpoint,
|
|
192
|
-
method = "GET",
|
|
193
|
-
body,
|
|
194
|
-
version = CONFIG.DEFAULT_VERSION,
|
|
195
|
-
query,
|
|
196
|
-
logger,
|
|
197
|
-
baseUrl = CONFIG.BASE_URL,
|
|
198
|
-
kyOptions
|
|
199
|
-
} = options;
|
|
200
|
-
const url = `${baseUrl}${storeHash}/${version}/${endpoint.replace(/^\//, "")}`;
|
|
201
|
-
const searchParams = query ? new URLSearchParams(query).toString() : "";
|
|
202
|
-
const fullUrl = searchParams ? `${url}?${searchParams}` : url;
|
|
203
|
-
if (fullUrl.length > CONFIG.MAX_URL_LENGTH) {
|
|
204
|
-
logger?.error(
|
|
205
|
-
{
|
|
206
|
-
urlLength: fullUrl.length,
|
|
207
|
-
maxLength: CONFIG.MAX_URL_LENGTH
|
|
208
|
-
},
|
|
209
|
-
"URL length exceeds maximum allowed length"
|
|
210
|
-
);
|
|
211
|
-
throw new RequestError(
|
|
212
|
-
400,
|
|
213
|
-
"URL too long",
|
|
214
|
-
`URL length ${fullUrl.length} exceeds maximum allowed length of ${CONFIG.MAX_URL_LENGTH}`
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
const request2 = {
|
|
218
|
-
method,
|
|
219
|
-
headers: {
|
|
220
|
-
"Content-Type": "application/json",
|
|
221
|
-
Accept: "application/json",
|
|
222
|
-
"X-Auth-Token": accessToken
|
|
223
|
-
},
|
|
224
|
-
json: body,
|
|
225
|
-
...kyOptions
|
|
226
|
-
};
|
|
227
|
-
const response = await ky(fullUrl, request2);
|
|
228
|
-
return response;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// src/util.ts
|
|
232
|
-
var chunkStrLength = (items, options = {}) => {
|
|
233
|
-
const { maxLength = 2048, chunkLength = 250, offset = 0, separatorSize = 1 } = options;
|
|
234
|
-
const chunks = [];
|
|
235
|
-
let currentStrLength = offset;
|
|
236
|
-
let currentChunk = [];
|
|
237
|
-
for (const item of items) {
|
|
238
|
-
const itemLength = encodeURIComponent(item).length;
|
|
239
|
-
const separatorLength = currentChunk.length > 0 ? separatorSize : 0;
|
|
240
|
-
const totalItemLength = itemLength + separatorLength;
|
|
241
|
-
const wouldExceedLength = currentStrLength + totalItemLength > maxLength;
|
|
242
|
-
const wouldExceedCount = currentChunk.length >= chunkLength;
|
|
243
|
-
if ((wouldExceedLength || wouldExceedCount) && currentChunk.length > 0) {
|
|
244
|
-
chunks.push(currentChunk);
|
|
245
|
-
currentChunk = [];
|
|
246
|
-
currentStrLength = offset;
|
|
247
|
-
}
|
|
248
|
-
if (itemLength + offset > maxLength) {
|
|
249
|
-
throw new Error(`Item too large: ${itemLength} exceeds maxLength ${maxLength}`);
|
|
250
|
-
}
|
|
251
|
-
currentChunk.push(item);
|
|
252
|
-
currentStrLength += totalItemLength;
|
|
253
|
-
}
|
|
254
|
-
if (currentChunk.length > 0) {
|
|
255
|
-
chunks.push(currentChunk);
|
|
256
|
-
}
|
|
257
|
-
return chunks;
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
// src/client.ts
|
|
261
|
-
var MAX_PAGE_SIZE = 250;
|
|
262
|
-
var DEFAULT_CONCURRENCY = 10;
|
|
263
|
-
function chunkArray(array, size) {
|
|
264
|
-
return Array.from({ length: Math.ceil(array.length / size) }, (_, i) => array.slice(i * size, i * size + size));
|
|
265
|
-
}
|
|
266
|
-
function rangeArray(start, end) {
|
|
267
|
-
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
268
|
-
}
|
|
269
|
-
var BigCommerceClient = class {
|
|
270
|
-
/**
|
|
271
|
-
* Creates a new BigCommerce client instance
|
|
272
|
-
* @param config - Configuration options for the client
|
|
273
|
-
* @param config.baseUrl - The base URL to use for the client (default: https://api.bigcommerce.com)
|
|
274
|
-
* @param config.storeHash - The store hash to use for the client
|
|
275
|
-
* @param config.accessToken - The API access token to use for the client
|
|
276
|
-
* @param config.maxRetries - The maximum number of retries for rate limit errors (default: 5)
|
|
277
|
-
* @param config.maxDelay - Maximum time to wait to retry in case of rate limit errors in milliseconds (default: 60000 - 1 minute). If `X-Rate-Limit-Time-Reset-Ms` header is higher than `maxDelay`, the request will fail immediately.
|
|
278
|
-
* @param config.concurrency - The default concurrency for concurrent methods (default: 10)
|
|
279
|
-
* @param config.skipErrors - Whether to skip errors during concurrent requests (default: false)
|
|
280
|
-
* @param config.logger - Optional logger instance for debugging and error tracking
|
|
281
|
-
*/
|
|
282
|
-
constructor(config) {
|
|
283
|
-
this.config = config;
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Makes a GET request to the BigCommerce API
|
|
287
|
-
* @param endpoint - The API endpoint to request
|
|
288
|
-
* @param options.query - Query parameters to include in the request
|
|
289
|
-
* @param options.version - API version to use (v2 or v3) (default: v3)
|
|
290
|
-
* @returns Promise resolving to the response data of type `R`
|
|
291
|
-
*/
|
|
292
|
-
async get(endpoint, options) {
|
|
293
|
-
return request({
|
|
294
|
-
endpoint,
|
|
295
|
-
method: "GET",
|
|
296
|
-
...options,
|
|
297
|
-
...this.config
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Makes a POST request to the BigCommerce API
|
|
302
|
-
* @param endpoint - The API endpoint to request
|
|
303
|
-
* @param options.query - Query parameters to include in the request
|
|
304
|
-
* @param options.version - API version to use (v2 or v3) (default: v3)
|
|
305
|
-
* @param options.body - Request body data of type `T`
|
|
306
|
-
* @returns Promise resolving to the response data of type `R`
|
|
307
|
-
*/
|
|
308
|
-
async post(endpoint, options) {
|
|
309
|
-
return request({
|
|
310
|
-
endpoint,
|
|
311
|
-
method: "POST",
|
|
312
|
-
...options,
|
|
313
|
-
...this.config
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Makes a PUT request to the BigCommerce API
|
|
318
|
-
* @param endpoint - The API endpoint to request
|
|
319
|
-
* @param options.query - Query parameters to include in the request
|
|
320
|
-
* @param options.version - API version to use (v2 or v3) (default: v3)
|
|
321
|
-
* @param options.body - Request body data of type `T`
|
|
322
|
-
* @returns Promise resolving to the response data of type `R`
|
|
323
|
-
*/
|
|
324
|
-
async put(endpoint, options) {
|
|
325
|
-
return request({
|
|
326
|
-
endpoint,
|
|
327
|
-
method: "PUT",
|
|
328
|
-
...options,
|
|
329
|
-
...this.config
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Makes a DELETE request to the BigCommerce API
|
|
334
|
-
* @param endpoint - The API endpoint to delete
|
|
335
|
-
* @param options.version - API version to use (v2 or v3) (default: v3)
|
|
336
|
-
* @returns Promise resolving to void
|
|
337
|
-
*/
|
|
338
|
-
async delete(endpoint, options) {
|
|
339
|
-
await request({
|
|
340
|
-
endpoint,
|
|
341
|
-
method: "DELETE",
|
|
342
|
-
...options,
|
|
343
|
-
...this.config
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Executes multiple requests concurrently with controlled concurrency
|
|
348
|
-
* @param requests - Array of request options to execute
|
|
349
|
-
* @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
|
|
350
|
-
* @param options.skipErrors - Whether to skip errors and continue processing (the errors will be logged if logger is provided), overrides the client's skipErrors setting (default: false)
|
|
351
|
-
* @returns Promise resolving to array of response data
|
|
352
|
-
*/
|
|
353
|
-
async concurrent(requests, options) {
|
|
354
|
-
const skipErrors = options?.skipErrors ?? this.config.skipErrors ?? false;
|
|
355
|
-
const results = await this.concurrentSettled(requests, options);
|
|
356
|
-
const successfulResults = [];
|
|
357
|
-
for (const result of results) {
|
|
358
|
-
if (result.status === "fulfilled") {
|
|
359
|
-
successfulResults.push(result.value);
|
|
360
|
-
} else {
|
|
361
|
-
if (!skipErrors) {
|
|
362
|
-
throw result.reason;
|
|
363
|
-
} else {
|
|
364
|
-
this.config.logger?.warn(
|
|
365
|
-
{
|
|
366
|
-
error: result.reason
|
|
367
|
-
},
|
|
368
|
-
"Error in concurrent request"
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return successfulResults;
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Lowest level concurrent request method.
|
|
377
|
-
* This method executes requests in chunks and returns bare PromiseSettledResult objects.
|
|
378
|
-
* Use this method if you need to handle errors in a custom way.
|
|
379
|
-
* @param requests - Array of request options to execute
|
|
380
|
-
* @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
|
|
381
|
-
* @returns Promise resolving to array of PromiseSettledResult containing both successful and failed requests
|
|
382
|
-
*/
|
|
383
|
-
async concurrentSettled(requests, options) {
|
|
384
|
-
const chunkSize = options?.concurrency ?? this.config.concurrency ?? DEFAULT_CONCURRENCY;
|
|
385
|
-
const chunks = chunkArray(requests, chunkSize);
|
|
386
|
-
this.config.logger?.debug(
|
|
387
|
-
{
|
|
388
|
-
totalRequests: requests.length,
|
|
389
|
-
chunkSize,
|
|
390
|
-
chunks: chunks.length
|
|
391
|
-
},
|
|
392
|
-
"Starting concurrent requests with detailed results"
|
|
393
|
-
);
|
|
394
|
-
const allResults = [];
|
|
395
|
-
for (const [index, chunk] of chunks.entries()) {
|
|
396
|
-
const responses = await Promise.allSettled(
|
|
397
|
-
chunk.map(
|
|
398
|
-
(opt) => request({
|
|
399
|
-
...opt,
|
|
400
|
-
...this.config
|
|
401
|
-
})
|
|
402
|
-
)
|
|
403
|
-
);
|
|
404
|
-
this.config.logger?.debug(
|
|
405
|
-
{
|
|
406
|
-
chunkIndex: index,
|
|
407
|
-
chunkSize: chunk.length,
|
|
408
|
-
totalRequests: requests.length,
|
|
409
|
-
totalChunks: chunks.length,
|
|
410
|
-
responses: responses.map((response) => response.status)
|
|
411
|
-
},
|
|
412
|
-
"Completed chunk"
|
|
413
|
-
);
|
|
414
|
-
allResults.push(...responses);
|
|
415
|
-
}
|
|
416
|
-
return allResults;
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Collects all pages of data from a paginated v3 API endpoint.
|
|
420
|
-
* This method pulls the first page and uses pagination meta to collect the remaining pages concurrently.
|
|
421
|
-
* @param endpoint - The API endpoint to request
|
|
422
|
-
* @param options.query - Query parameters to include in the request
|
|
423
|
-
* @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
|
|
424
|
-
* @param options.skipErrors - Whether to skip errors and continue processing (the errors will be logged if logger is provided), overrides the client's skipErrors setting (default: false)
|
|
425
|
-
* @returns Promise resolving to array of all items across all pages
|
|
426
|
-
*/
|
|
427
|
-
async collect(endpoint, options) {
|
|
428
|
-
options = options ?? {};
|
|
429
|
-
if (options.query) {
|
|
430
|
-
if (!options.query.limit) {
|
|
431
|
-
options.query.limit = MAX_PAGE_SIZE.toString();
|
|
432
|
-
}
|
|
433
|
-
} else {
|
|
434
|
-
options.query = { limit: MAX_PAGE_SIZE.toString() };
|
|
435
|
-
}
|
|
436
|
-
const first = await this.get(endpoint, options);
|
|
437
|
-
if (!Array.isArray(first.data) || !first?.meta?.pagination?.total_pages) {
|
|
438
|
-
return first.data;
|
|
439
|
-
}
|
|
440
|
-
const results = [...first.data];
|
|
441
|
-
const pages = first.meta.pagination.total_pages;
|
|
442
|
-
if (pages > 1) {
|
|
443
|
-
this.config.logger?.debug(
|
|
444
|
-
{
|
|
445
|
-
totalPages: pages,
|
|
446
|
-
itemsPerPage: first.data.length
|
|
447
|
-
},
|
|
448
|
-
"Collecting remaining pages"
|
|
449
|
-
);
|
|
450
|
-
const pageRequests = rangeArray(2, pages).map((page) => ({
|
|
451
|
-
endpoint,
|
|
452
|
-
method: "GET",
|
|
453
|
-
query: {
|
|
454
|
-
...options.query,
|
|
455
|
-
page: page.toString()
|
|
456
|
-
},
|
|
457
|
-
kyOptions: options.kyOptions
|
|
458
|
-
}));
|
|
459
|
-
const remainingPages = await this.concurrent(pageRequests, options);
|
|
460
|
-
remainingPages.forEach((page) => {
|
|
461
|
-
if (Array.isArray(page.data)) {
|
|
462
|
-
results.push(...page.data);
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
return results;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Collects all pages of data from a paginated v2 API endpoint.
|
|
470
|
-
* This method simply pulls all pages concurrently until a 204 is returned in a batch.
|
|
471
|
-
* @param endpoint - The API endpoint to request
|
|
472
|
-
* @param options.query - Query parameters to include in the request
|
|
473
|
-
* @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
|
|
474
|
-
* @param options.skipErrors - Whether to skip errors and continue processing (the errors will be logged if logger is provided), overrides the client's skipErrors setting (default: false)
|
|
475
|
-
* @returns Promise resolving to array of all items across all pages
|
|
476
|
-
*/
|
|
477
|
-
async collectV2(endpoint, options) {
|
|
478
|
-
options = options ?? {};
|
|
479
|
-
if (options.query) {
|
|
480
|
-
if (!options.query.limit) {
|
|
481
|
-
options.query.limit = MAX_PAGE_SIZE.toString();
|
|
482
|
-
}
|
|
483
|
-
} else {
|
|
484
|
-
options.query = { limit: MAX_PAGE_SIZE.toString() };
|
|
485
|
-
}
|
|
486
|
-
let done = false;
|
|
487
|
-
const results = [];
|
|
488
|
-
let page = 1;
|
|
489
|
-
const concurrency = options.concurrency ?? this.config.concurrency ?? DEFAULT_CONCURRENCY;
|
|
490
|
-
while (!done) {
|
|
491
|
-
const pages = rangeArray(page, page + concurrency);
|
|
492
|
-
page += concurrency;
|
|
493
|
-
const requests = pages.map((page2) => ({
|
|
494
|
-
...options,
|
|
495
|
-
endpoint,
|
|
496
|
-
version: "v2",
|
|
497
|
-
query: { ...options.query, page: page2.toString() }
|
|
498
|
-
}));
|
|
499
|
-
const responses = await Promise.allSettled(requests.map((request2) => this.get(endpoint, request2)));
|
|
500
|
-
responses.forEach((response) => {
|
|
501
|
-
if (response.status === "fulfilled") {
|
|
502
|
-
if (response.value) {
|
|
503
|
-
results.push(...response.value);
|
|
504
|
-
} else {
|
|
505
|
-
done = true;
|
|
506
|
-
}
|
|
507
|
-
} else {
|
|
508
|
-
if (response.reason instanceof RequestError && response.reason.status === 404) {
|
|
509
|
-
done = true;
|
|
510
|
-
} else {
|
|
511
|
-
if (!(options.skipErrors ?? this.config.skipErrors ?? false)) {
|
|
512
|
-
throw response.reason;
|
|
513
|
-
} else {
|
|
514
|
-
this.config.logger?.warn(
|
|
515
|
-
{
|
|
516
|
-
error: response.reason instanceof Error ? {
|
|
517
|
-
name: response.reason.name,
|
|
518
|
-
message: response.reason.message
|
|
519
|
-
} : response.reason
|
|
520
|
-
},
|
|
521
|
-
"Error in collectV2"
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
return results;
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Queries multiple values against a single field using the v3 API.
|
|
532
|
-
* If the url + query params are too long, the query will be chunked. Otherwise, this method acts like `collect`.
|
|
533
|
-
* This method does not check for uniqueness of the `values` array.
|
|
534
|
-
*
|
|
535
|
-
* @param endpoint - The API endpoint to request
|
|
536
|
-
* @param options.key - The field name to query against e.g. `sku:in`
|
|
537
|
-
* @param options.values - Array of values to query for e.g. `['123', '456', ...]`
|
|
538
|
-
* @param options.query - Additional query parameters
|
|
539
|
-
* @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
|
|
540
|
-
* @param options.skipErrors - Whether to skip errors and continue processing (the errors will be logged if logger is provided), overrides the client's skipErrors setting (default: false)
|
|
541
|
-
* @returns Promise resolving to array of matching items
|
|
542
|
-
*/
|
|
543
|
-
async query(endpoint, options) {
|
|
544
|
-
if (options.query) {
|
|
545
|
-
if (!options.query.limit) {
|
|
546
|
-
options.query.limit = MAX_PAGE_SIZE.toString();
|
|
547
|
-
}
|
|
548
|
-
} else {
|
|
549
|
-
options.query = { limit: MAX_PAGE_SIZE.toString() };
|
|
550
|
-
}
|
|
551
|
-
const keySize = encodeURIComponent(options.key).length;
|
|
552
|
-
const fullUrl = `${BASE_URL}${this.config.storeHash}/v3/${endpoint}?${new URLSearchParams(options.query).toString()}`;
|
|
553
|
-
const offset = fullUrl.length + keySize + 1;
|
|
554
|
-
const chunkLength = Number.parseInt(options.query?.limit) || MAX_PAGE_SIZE;
|
|
555
|
-
const separatorSize = encodeURIComponent(",").length;
|
|
556
|
-
const queryStr = options.values.map((value) => `${value}`);
|
|
557
|
-
const chunks = chunkStrLength(queryStr, {
|
|
558
|
-
separatorSize,
|
|
559
|
-
offset,
|
|
560
|
-
chunkLength
|
|
561
|
-
});
|
|
562
|
-
this.config.logger?.debug(
|
|
563
|
-
{
|
|
564
|
-
offset,
|
|
565
|
-
totalValues: options.values.length,
|
|
566
|
-
chunks: chunks.length,
|
|
567
|
-
valuesPerChunk: chunks[0]?.length,
|
|
568
|
-
separatorSize
|
|
569
|
-
},
|
|
570
|
-
"Querying with chunked values"
|
|
571
|
-
);
|
|
572
|
-
const requests = chunks.map((chunk) => ({
|
|
573
|
-
...options,
|
|
574
|
-
endpoint,
|
|
575
|
-
query: { ...options.query, [options.key]: chunk.join(",") }
|
|
576
|
-
}));
|
|
577
|
-
const responses = await this.concurrent(requests, options);
|
|
578
|
-
return responses.flatMap((response) => response.data);
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
// src/auth.ts
|
|
583
|
-
import ky2, { HTTPError as HTTPError2 } from "ky";
|
|
584
|
-
import * as jose from "jose";
|
|
585
|
-
var GRANT_TYPE = "authorization_code";
|
|
586
|
-
var TOKEN_ENDPOINT = "https://login.bigcommerce.com/oauth2/token";
|
|
587
|
-
var ISSUER = "bc";
|
|
588
|
-
var BigCommerceAuth = class {
|
|
589
|
-
/**
|
|
590
|
-
* Creates a new BigCommerceAuth instance for handling OAuth authentication
|
|
591
|
-
* @param config - Configuration options for BigCommerce authentication
|
|
592
|
-
* @param config.clientId - The OAuth client ID from BigCommerce
|
|
593
|
-
* @param config.secret - The OAuth client secret from BigCommerce
|
|
594
|
-
* @param config.redirectUri - The redirect URI registered with BigCommerce
|
|
595
|
-
* @param config.scopes - Optional array of scopes to validate during auth callback
|
|
596
|
-
* @param config.logger - Optional logger instance for debugging and error tracking
|
|
597
|
-
* @throws {Error} If the redirect URI is invalid
|
|
598
|
-
*/
|
|
599
|
-
constructor(config) {
|
|
600
|
-
this.config = config;
|
|
601
|
-
try {
|
|
602
|
-
new URL(this.config.redirectUri);
|
|
603
|
-
} catch (error) {
|
|
604
|
-
throw new Error("Invalid redirect URI", { cause: error });
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* Requests an access token from BigCommerce
|
|
609
|
-
* @param data - Either a query string, URLSearchParams, or AuthQuery object containing auth callback data
|
|
610
|
-
* @returns Promise resolving to the token response
|
|
611
|
-
*/
|
|
612
|
-
async requestToken(data) {
|
|
613
|
-
const query = typeof data === "string" || data instanceof URLSearchParams ? this.parseQueryString(data) : data;
|
|
614
|
-
this.validateScopes(query.scope);
|
|
615
|
-
const tokenRequest = {
|
|
616
|
-
client_id: this.config.clientId,
|
|
617
|
-
client_secret: this.config.secret,
|
|
618
|
-
...query,
|
|
619
|
-
grant_type: GRANT_TYPE,
|
|
620
|
-
redirect_uri: this.config.redirectUri
|
|
621
|
-
};
|
|
622
|
-
this.config.logger?.debug(
|
|
623
|
-
{
|
|
624
|
-
clientId: this.config.clientId,
|
|
625
|
-
context: query.context,
|
|
626
|
-
scopes: query.scope
|
|
627
|
-
},
|
|
628
|
-
"Requesting OAuth token"
|
|
629
|
-
);
|
|
630
|
-
let res;
|
|
631
|
-
try {
|
|
632
|
-
res = await ky2(TOKEN_ENDPOINT, {
|
|
633
|
-
method: "POST",
|
|
634
|
-
json: tokenRequest
|
|
635
|
-
});
|
|
636
|
-
} catch (error) {
|
|
637
|
-
if (error instanceof HTTPError2) {
|
|
638
|
-
const text = await error.response.text();
|
|
639
|
-
this.config.logger?.error({
|
|
640
|
-
err: {
|
|
641
|
-
name: error.name,
|
|
642
|
-
message: error.message,
|
|
643
|
-
text
|
|
644
|
-
}
|
|
645
|
-
});
|
|
646
|
-
throw new Error(`Failed to request token. BC returned: ${text}`, { cause: error });
|
|
647
|
-
}
|
|
648
|
-
this.config.logger?.error({
|
|
649
|
-
err: error instanceof Error ? {
|
|
650
|
-
name: error.name,
|
|
651
|
-
message: error.message
|
|
652
|
-
} : error
|
|
653
|
-
});
|
|
654
|
-
throw new Error(`Failed to request token`, { cause: error });
|
|
655
|
-
}
|
|
656
|
-
return res.json();
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* Verifies a JWT payload from BigCommerce
|
|
660
|
-
* @param jwtPayload - The JWT string to verify
|
|
661
|
-
* @param storeHash - The store hash for the BigCommerce store
|
|
662
|
-
* @returns Promise resolving to the verified JWT claims
|
|
663
|
-
* @throws {Error} If the JWT is invalid
|
|
664
|
-
*/
|
|
665
|
-
async verify(jwtPayload, storeHash) {
|
|
666
|
-
try {
|
|
667
|
-
const secret = new TextEncoder().encode(this.config.secret);
|
|
668
|
-
const { payload } = await jose.jwtVerify(jwtPayload, secret, {
|
|
669
|
-
audience: this.config.clientId,
|
|
670
|
-
issuer: ISSUER,
|
|
671
|
-
subject: `stores/${storeHash}`
|
|
672
|
-
});
|
|
673
|
-
this.config.logger?.debug(
|
|
674
|
-
{
|
|
675
|
-
userId: payload.user?.id,
|
|
676
|
-
storeHash: payload.sub.split("/")[1]
|
|
677
|
-
},
|
|
678
|
-
"JWT verified successfully"
|
|
679
|
-
);
|
|
680
|
-
return payload;
|
|
681
|
-
} catch (error) {
|
|
682
|
-
this.config.logger?.error({
|
|
683
|
-
error: error instanceof Error ? {
|
|
684
|
-
name: error.name,
|
|
685
|
-
message: error.message
|
|
686
|
-
} : error
|
|
687
|
-
});
|
|
688
|
-
throw new Error("Invalid JWT payload", { cause: error });
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
|
-
* Parses and validates a query string from BigCommerce auth callback
|
|
693
|
-
* @param queryString - The query string to parse
|
|
694
|
-
* @returns The parsed auth query parameters
|
|
695
|
-
* @throws {Error} If required parameters are missing or scopes are invalid
|
|
696
|
-
*/
|
|
697
|
-
parseQueryString(queryString) {
|
|
698
|
-
const params = typeof queryString === "string" ? new URLSearchParams(queryString) : queryString;
|
|
699
|
-
const code = params.get("code");
|
|
700
|
-
const scope = params.get("scope");
|
|
701
|
-
const context = params.get("context");
|
|
702
|
-
if (!code) {
|
|
703
|
-
throw new Error("No code found in query string");
|
|
704
|
-
}
|
|
705
|
-
if (!scope) {
|
|
706
|
-
throw new Error("No scope found in query string");
|
|
707
|
-
} else if (this.config.scopes?.length) {
|
|
708
|
-
this.validateScopes(scope);
|
|
709
|
-
}
|
|
710
|
-
if (!context) {
|
|
711
|
-
throw new Error("No context found in query string");
|
|
712
|
-
}
|
|
713
|
-
return {
|
|
714
|
-
code,
|
|
715
|
-
scope,
|
|
716
|
-
context
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Validates that the granted scopes match the expected scopes
|
|
721
|
-
* @param scopes - Space-separated list of granted scopes
|
|
722
|
-
* @throws {Error} If the scopes don't match the expected scopes
|
|
723
|
-
*/
|
|
724
|
-
validateScopes(scopes) {
|
|
725
|
-
if (!this.config.scopes) {
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
const grantedScopes = scopes.split(" ");
|
|
729
|
-
const requiredScopes = this.config.scopes;
|
|
730
|
-
const missingScopes = requiredScopes.filter((scope) => !grantedScopes.includes(scope));
|
|
731
|
-
if (missingScopes.length) {
|
|
732
|
-
throw new Error(`Scope mismatch: ${scopes}; expected: ${this.config.scopes.join(" ")}`);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
export {
|
|
737
|
-
BigCommerceAuth,
|
|
738
|
-
BigCommerceClient,
|
|
739
|
-
Methods
|
|
740
|
-
};
|
|
741
|
-
//# sourceMappingURL=index.js.map
|