@atomic-solutions/woocommerce-api-client 0.4.0 → 0.5.1

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.
@@ -1,7 +1,14 @@
1
- import axios, { isAxiosError } from 'axios';
1
+ import axios, { AxiosError, isCancel, isAxiosError } from 'axios';
2
+ import { BaseError as BaseError$1 } from '@atomic-solutions/errors';
2
3
  import { z } from 'zod';
3
4
 
4
- // src/client/createClient.ts
5
+ var __typeError = (msg) => {
6
+ throw TypeError(msg);
7
+ };
8
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
9
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
10
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
11
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
5
12
 
6
13
  // src/errors/classes/BaseError.ts
7
14
  var BaseError = class extends Error {
@@ -130,8 +137,16 @@ var WOOCOMMERCE_ERROR_METADATA = {
130
137
  }
131
138
  };
132
139
 
140
+ // src/errors/classes/http-error.ts
141
+ var redactPath = (url) => url.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, "").replace(/[?#].*$/, "").split("/").map((segment) => /^(\d+|[0-9a-f]{16,}|[0-9a-f-]{36})$/i.test(segment) ? ":id" : segment).join("/");
142
+ var classifyHttpStatus = (status) => ({
143
+ domain: status === void 0 ? "network" : status === 401 || status === 403 ? "auth" : "integration",
144
+ severity: status !== void 0 && status < 500 ? "warning" : "error"
145
+ });
146
+
133
147
  // src/errors/classes/WooCommerceApiError.ts
134
- var WooCommerceApiError = class extends BaseError {
148
+ var _options;
149
+ var WooCommerceApiError = class extends BaseError$1 {
135
150
  constructor(options) {
136
151
  const metadata = WOOCOMMERCE_ERROR_METADATA[options.code] ?? {
137
152
  operation: "woocommerce_api_request",
@@ -143,7 +158,30 @@ var WooCommerceApiError = class extends BaseError {
143
158
  userMessage: options.userMessage || metadata.userMessage,
144
159
  retryable: options.retryable ?? metadata.retryable
145
160
  };
146
- super(enrichedOptions);
161
+ const status = enrichedOptions.statusCode;
162
+ const { domain, severity } = classifyHttpStatus(status);
163
+ super({
164
+ domain,
165
+ severity,
166
+ operation: enrichedOptions.operation,
167
+ message: enrichedOptions.message,
168
+ code: enrichedOptions.code,
169
+ retryable: enrichedOptions.retryable,
170
+ userMessage: enrichedOptions.userMessage,
171
+ cause: enrichedOptions.cause,
172
+ fingerprint: [domain, enrichedOptions.code, String(status ?? "network")],
173
+ extras: {
174
+ originalCode: enrichedOptions.originalCode,
175
+ statusCode: status,
176
+ path: redactPath(enrichedOptions.url),
177
+ method: enrichedOptions.method
178
+ }
179
+ });
180
+ __privateAdd(this, _options);
181
+ __privateSet(this, _options, enrichedOptions);
182
+ }
183
+ get options() {
184
+ return __privateGet(this, _options);
147
185
  }
148
186
  // Convenient getters for frequently accessed fields (no duplication)
149
187
  get wooCode() {
@@ -170,24 +208,8 @@ var WooCommerceApiError = class extends BaseError {
170
208
  get data() {
171
209
  return this.options.data;
172
210
  }
173
- /**
174
- * Get reportable data for error tracking/logging
175
- * Includes HTTP context and WooCommerce-specific fields
176
- */
177
- getReportableData() {
178
- return {
179
- code: this.options.code,
180
- operation: this.options.operation,
181
- userMessage: this.options.userMessage,
182
- retryable: this.options.retryable,
183
- statusCode: this.options.statusCode,
184
- url: this.options.url,
185
- method: this.options.method,
186
- wooCode: this.options.code,
187
- originalCode: this.options.originalCode
188
- };
189
- }
190
211
  };
212
+ _options = new WeakMap();
191
213
 
192
214
  // src/errors/classes/WooCommerceDataValidationError.ts
193
215
  var WooCommerceDataValidationError = class _WooCommerceDataValidationError extends BaseError {
@@ -278,34 +300,34 @@ var WooCommerceDataValidationError = class _WooCommerceDataValidationError exten
278
300
  };
279
301
 
280
302
  // src/http/handleApiResponse.ts
281
- var handleApiResponse = (response, schema, options) => {
303
+ var handleApiData = (data, schema, options, url) => {
282
304
  const validationMode = options?.validationMode ?? "strict";
283
305
  if (validationMode === "warn") {
284
- const result = schema.safeParse(response.data);
306
+ const result = schema.safeParse(data);
285
307
  if (!result.success) {
286
308
  const validationError = new WooCommerceDataValidationError({
287
- message: `WooCommerce API response validation failed for ${response.config.url || "unknown endpoint"}`,
288
- url: response.config.url,
309
+ message: `WooCommerce API response validation failed for ${url || "unknown endpoint"}`,
310
+ url,
289
311
  zodError: result.error,
290
- value: response.data,
312
+ value: data,
291
313
  userMessage: "Received unexpected data from WooCommerce"
292
314
  });
293
315
  options?.onValidationError?.(validationError);
294
316
  options?.errorReporter?.(validationError);
295
317
  console.warn("[woocommerce-utils] Validation warning:", validationError.message);
296
- return response.data;
318
+ return data;
297
319
  }
298
320
  return result.data;
299
321
  }
300
322
  try {
301
- return schema.parse(response.data);
323
+ return schema.parse(data);
302
324
  } catch (error2) {
303
325
  if (error2 && typeof error2 === "object" && "issues" in error2) {
304
326
  const validationError = new WooCommerceDataValidationError({
305
- message: `WooCommerce API response validation failed for ${response.config.url || "unknown endpoint"}`,
306
- url: response.config.url,
327
+ message: `WooCommerce API response validation failed for ${url || "unknown endpoint"}`,
328
+ url,
307
329
  zodError: error2,
308
- value: response.data,
330
+ value: data,
309
331
  userMessage: "Received unexpected data from WooCommerce"
310
332
  });
311
333
  options?.onValidationError?.(validationError);
@@ -315,6 +337,7 @@ var handleApiResponse = (response, schema, options) => {
315
337
  throw error2;
316
338
  }
317
339
  };
340
+ var handleApiResponse = (response, schema, options) => handleApiData(response.data, schema, options, response.config.url);
318
341
 
319
342
  // src/utils/calculatePagination.ts
320
343
  var calculatePagination = (input) => {
@@ -429,6 +452,12 @@ var error = (...args) => {
429
452
  console.error(DEBUG_PREFIX, ...args);
430
453
  };
431
454
 
455
+ // src/http/storeRoutes.ts
456
+ var CART_ROUTE = /\/wc\/store\/(v\d+\/)?(cart|checkout|batch)/;
457
+ var BATCH_ROUTE = /\/wc\/store\/(v\d+\/)?batch/;
458
+ var isCartRoute = (url) => !!url && CART_ROUTE.test(url);
459
+ var isBatchRoute = (url) => !!url && BATCH_ROUTE.test(url);
460
+
432
461
  // src/http/interceptors/request.ts
433
462
  var requiresAuth = (url) => {
434
463
  if (!url) return false;
@@ -442,30 +471,26 @@ var requiresAuth = (url) => {
442
471
  ];
443
472
  return authEndpoints.some((endpoint) => url.includes(endpoint));
444
473
  };
445
- var requiresCartAuth = (url) => {
446
- if (!url) return false;
447
- return /\/wc\/store\/(v\d+\/)?cart/.test(url) || /\/wc\/store\/(v\d+\/)?checkout/.test(url);
448
- };
449
474
  var setupRequestInterceptor = (axiosInstance, config, client) => {
450
475
  axiosInstance.interceptors.request.use(
451
476
  async (requestConfig) => {
452
477
  const url = requestConfig.url;
453
478
  if (config.jwtToken) {
454
479
  const token = await config.jwtToken.get();
455
- if (token && (requiresAuth(url) || requiresCartAuth(url))) {
480
+ if (token && (requiresAuth(url) || isCartRoute(url))) {
456
481
  requestConfig.headers.Authorization = `Bearer ${token}`;
457
482
  if (config.debug) {
458
483
  debug("Injected JWT token for", url);
459
484
  }
460
485
  }
461
486
  }
462
- if (requiresCartAuth(url)) {
487
+ if (isCartRoute(url)) {
463
488
  const headers = await config.cartHeaders.get();
464
489
  const isAuthenticated = !!await config.jwtToken?.get();
465
490
  if (headers.nonce) {
466
491
  requestConfig.headers.Nonce = headers.nonce;
467
492
  }
468
- if (headers.cartToken && !isAuthenticated) {
493
+ if (headers.cartToken && (!isAuthenticated || isBatchRoute(url))) {
469
494
  requestConfig.headers["Cart-Token"] = headers.cartToken;
470
495
  }
471
496
  requestConfig.headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
@@ -494,16 +519,19 @@ var setupRequestInterceptor = (axiosInstance, config, client) => {
494
519
  }
495
520
  );
496
521
  };
497
-
498
- // src/http/interceptors/response.ts
499
- var isCartResponse = (url) => {
500
- if (!url) return false;
501
- return /\/wc\/store\/(v\d+\/)?cart/.test(url) || /\/wc\/store\/(v\d+\/)?checkout/.test(url);
522
+ var NONCE_ERROR_CODES = ["woocommerce_rest_missing_nonce", "woocommerce_rest_invalid_nonce"];
523
+ var isNonceRejectedBatch = (data) => {
524
+ const responses = data?.responses;
525
+ if (!Array.isArray(responses) || responses.length === 0) return false;
526
+ return responses.every((response) => {
527
+ const code = response?.body?.code;
528
+ return typeof code === "string" && NONCE_ERROR_CODES.includes(code);
529
+ });
502
530
  };
503
531
  var setupResponseInterceptor = (axiosInstance, config, client) => {
504
532
  axiosInstance.interceptors.response.use(
505
533
  async (response) => {
506
- if (isCartResponse(response.config.url)) {
534
+ if (isCartRoute(response.config.url)) {
507
535
  const nonce = response.headers["nonce"] || response.headers["Nonce"];
508
536
  const cartToken = response.headers["cart-token"] || response.headers["Cart-Token"];
509
537
  if (nonce || cartToken) {
@@ -523,6 +551,15 @@ var setupResponseInterceptor = (axiosInstance, config, client) => {
523
551
  }
524
552
  }
525
553
  }
554
+ if (isBatchRoute(response.config.url) && isNonceRejectedBatch(response.data)) {
555
+ throw new AxiosError(
556
+ "Batch rejected for a nonce error",
557
+ AxiosError.ERR_BAD_REQUEST,
558
+ response.config,
559
+ response.request,
560
+ { ...response, status: 401, data: { code: "woocommerce_rest_missing_nonce" } }
561
+ );
562
+ }
526
563
  if (config.debug) {
527
564
  debug("Response:", response.config.url, response.status);
528
565
  }
@@ -565,12 +602,15 @@ var setupErrorInterceptor = (axiosInstance, config, client) => {
565
602
  axiosInstance.interceptors.response.use(
566
603
  (response) => response,
567
604
  async (axiosError) => {
568
- const status = axiosError.response?.status || 500;
605
+ if (isCancel(axiosError)) {
606
+ return Promise.reject(axiosError);
607
+ }
608
+ const status = axiosError.response?.status;
569
609
  const errorData = axiosError.response?.data;
610
+ const isNetworkError = isAxiosError(axiosError) && !axiosError.response;
570
611
  const apiCode = errorData?.code || "unknown_error";
571
612
  const wpMessage = errorData?.message || axiosError.message || "Unknown error";
572
- const errorCode = mapWooCommerceCode(apiCode);
573
- const errorMetadata = WOOCOMMERCE_ERROR_METADATA[errorCode];
613
+ const errorCode = isNetworkError ? "network_error" : mapWooCommerceCode(apiCode);
574
614
  const wcError = new WooCommerceApiError({
575
615
  message: wpMessage,
576
616
  statusCode: status,
@@ -578,9 +618,6 @@ var setupErrorInterceptor = (axiosInstance, config, client) => {
578
618
  // Transformed app-level code
579
619
  originalCode: apiCode,
580
620
  // Original WooCommerce API code
581
- retryable: errorMetadata.retryable,
582
- userMessage: errorMetadata.userMessage,
583
- operation: errorMetadata.operation,
584
621
  url: axiosError.config?.url || "",
585
622
  method: axiosError.config?.method?.toUpperCase() || "GET",
586
623
  requestBody: axiosError.config?.data,
@@ -961,7 +998,7 @@ var cartSchema = z.object({
961
998
  cross_sells: z.array(z.unknown()),
962
999
  fees: z.array(z.unknown())
963
1000
  });
964
- z.object({
1001
+ var addToCartInputSchema = z.object({
965
1002
  id: z.number(),
966
1003
  quantity: z.number().int().positive(),
967
1004
  variation: z.array(
@@ -971,6 +1008,18 @@ z.object({
971
1008
  })
972
1009
  ).optional()
973
1010
  });
1011
+ z.object({
1012
+ items: z.array(addToCartInputSchema).min(1)
1013
+ });
1014
+ var batchResponseSchema = z.object({
1015
+ responses: z.array(
1016
+ z.object({
1017
+ status: z.number(),
1018
+ body: z.unknown(),
1019
+ headers: z.record(z.string(), z.unknown()).optional()
1020
+ })
1021
+ )
1022
+ });
974
1023
  z.object({
975
1024
  key: z.string(),
976
1025
  quantity: z.number().int().positive()
@@ -1330,6 +1379,19 @@ z.object({
1330
1379
  var ORDER_ATTRIBUTION_NAMESPACE = "woocommerce/order-attribution";
1331
1380
 
1332
1381
  // src/api/cart.ts
1382
+ var BATCH_MAX_REQUESTS = 25;
1383
+ var chunk = (list, size) => Array.from(
1384
+ { length: Math.ceil(list.length / size) },
1385
+ (_, index) => list.slice(index * size, index * size + size)
1386
+ );
1387
+ var isAccepted = (status) => status >= 200 && status < 300;
1388
+ var bodyError = (body) => {
1389
+ const data = body;
1390
+ return {
1391
+ code: typeof data?.code === "string" ? data.code : void 0,
1392
+ message: typeof data?.message === "string" ? data.message : void 0
1393
+ };
1394
+ };
1333
1395
  var createCartAPI = (client, endpoints, options) => ({
1334
1396
  get: async () => {
1335
1397
  const response = await client.get(endpoints.cart);
@@ -1339,6 +1401,48 @@ var createCartAPI = (client, endpoints, options) => ({
1339
1401
  const response = await client.post(endpoints.addItem, input);
1340
1402
  return handleApiResponse(response, cartSchema, options);
1341
1403
  },
1404
+ addItems: async ({ items }) => {
1405
+ const responses = [];
1406
+ let chunkFailed = false;
1407
+ for (const batch of chunk(items, BATCH_MAX_REQUESTS)) {
1408
+ try {
1409
+ const response = await client.post(endpoints.batch, {
1410
+ requests: batch.map((item) => ({
1411
+ method: "POST",
1412
+ path: endpoints.addItem,
1413
+ body: item
1414
+ }))
1415
+ });
1416
+ responses.push(...handleApiResponse(response, batchResponseSchema, options).responses);
1417
+ } catch (error2) {
1418
+ if (responses.length === 0) throw error2;
1419
+ chunkFailed = true;
1420
+ break;
1421
+ }
1422
+ }
1423
+ const results = items.map((item, index) => {
1424
+ const entry = responses[index];
1425
+ const { code, message } = bodyError(entry?.body);
1426
+ return {
1427
+ item,
1428
+ status: entry?.status ?? 500,
1429
+ ...code ? { code } : {},
1430
+ ...message ? { message } : {}
1431
+ };
1432
+ });
1433
+ const lastAccepted = [...responses].reverse().find((entry) => isAccepted(entry.status));
1434
+ const batchCart = lastAccepted ? handleApiData(lastAccepted.body, cartSchema, options, endpoints.batch) : void 0;
1435
+ let cart = chunkFailed ? void 0 : batchCart;
1436
+ if (!cart) {
1437
+ try {
1438
+ cart = handleApiResponse(await client.get(endpoints.cart), cartSchema, options);
1439
+ } catch (error2) {
1440
+ if (!batchCart) throw error2;
1441
+ cart = batchCart;
1442
+ }
1443
+ }
1444
+ return { cart, results };
1445
+ },
1342
1446
  updateItem: async (input) => {
1343
1447
  const response = await client.post(endpoints.updateItem, input);
1344
1448
  return handleApiResponse(response, cartSchema, options);
@@ -1428,6 +1532,8 @@ var createEndpoints = (version = "legacy") => {
1428
1532
  removeCoupon: `${prefix}/cart/remove-coupon`,
1429
1533
  updateCustomer: `${prefix}/cart/update-customer`,
1430
1534
  selectShippingRate: `${prefix}/cart/select-shipping-rate`,
1535
+ // Batch
1536
+ batch: `${prefix}/batch`,
1431
1537
  // Checkout
1432
1538
  checkout: `${prefix}/checkout`,
1433
1539
  // Orders