lightspeed-retail-sdk 3.3.4 → 3.3.5
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 +3 -3
- package/dist/src/core/LightspeedSDK.cjs +75 -0
- package/dist/src/core/LightspeedSDK.mjs +91 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A modern JavaScript SDK for interacting with the Lightspeed Retail API. This SDK provides a convenient, secure, and flexible way to access Lightspeed Retail's features—including customer, item, and order management.
|
|
4
4
|
|
|
5
|
-
**Current Version: 3.3.
|
|
5
|
+
**Current Version: 3.3.5** — improve error checking on token refresh to prevent false email warnings.
|
|
6
6
|
|
|
7
|
-
## **🆕 Recent Updates (v3.3.
|
|
7
|
+
## **🆕 Recent Updates (v3.3.5)**
|
|
8
8
|
|
|
9
9
|
- **Add centralized query param builder for API requests**: Add centralized query param builder for API requests. Supports input as object, string, or array, and manages relations/load_relations. Ensures no double-encoding of parameters and handles special cases for 'or' and 'timeStamp'.
|
|
10
10
|
- **🎯 Enhanced Parameter Support**: All main getter methods now support both legacy and new object-based parameters with full backward compatibility
|
|
@@ -66,7 +66,7 @@ const items = await sdk.getItems({
|
|
|
66
66
|
## Table of Contents
|
|
67
67
|
|
|
68
68
|
- [Another Unofficial Lightspeed Retail V3 API SDK](#another-unofficial-lightspeed-retail-v3-api-sdk)
|
|
69
|
-
- [**🆕 Recent Updates (v3.3.
|
|
69
|
+
- [**🆕 Recent Updates (v3.3.5)**](#-recent-updates-v335)
|
|
70
70
|
- [🚀 Key Features](#-key-features)
|
|
71
71
|
- [🔄 Migrating from 3.1.x](#-migrating-from-31x)
|
|
72
72
|
- [Backward Compatibility](#backward-compatibility)
|
|
@@ -291,6 +291,81 @@ class LightspeedSDKCore {
|
|
|
291
291
|
this.refreshInProgress = false; // Release the lock
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
+
// Core API request handler
|
|
295
|
+
async executeApiRequest(options, retries = 0) {
|
|
296
|
+
await this.handleRateLimit(options);
|
|
297
|
+
const token = await this.getToken();
|
|
298
|
+
if (!token) throw new Error("Error Fetching Token");
|
|
299
|
+
options.headers = {
|
|
300
|
+
Authorization: `Bearer ${token}`,
|
|
301
|
+
"Content-Type": "application/json",
|
|
302
|
+
...options.headers
|
|
303
|
+
};
|
|
304
|
+
// Centralized query param handling
|
|
305
|
+
if (options.params) {
|
|
306
|
+
const queryString = buildQueryParams(options.params);
|
|
307
|
+
if (queryString) {
|
|
308
|
+
// Remove any trailing ? or & from url
|
|
309
|
+
options.url = options.url.replace(/[?&]+$/, "");
|
|
310
|
+
options.url += (options.url.includes("?") ? "&" : "?") + queryString;
|
|
311
|
+
}
|
|
312
|
+
delete options.params; // Don't let axios try to re-encode
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const res = await (0, _axios.default)(options);
|
|
316
|
+
this.lastResponse = res;
|
|
317
|
+
if (options.method === "GET") {
|
|
318
|
+
var _res_data_attributes, _res_data_attributes1;
|
|
319
|
+
// Handle successful response with no data or empty data
|
|
320
|
+
if (!res.data || Object.keys(res.data).length === 0) {
|
|
321
|
+
return {
|
|
322
|
+
data: {},
|
|
323
|
+
next: null,
|
|
324
|
+
previous: null
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// Check if response has the expected structure but with empty arrays
|
|
328
|
+
const dataKeys = Object.keys(res.data).filter((key)=>key !== "@attributes");
|
|
329
|
+
if (dataKeys.length > 0) {
|
|
330
|
+
const firstDataKey = dataKeys[0];
|
|
331
|
+
const firstDataValue = res.data[firstDataKey];
|
|
332
|
+
// No need to log for empty arrays - this is normal
|
|
333
|
+
}
|
|
334
|
+
// Handle successful response with data
|
|
335
|
+
return {
|
|
336
|
+
data: res.data,
|
|
337
|
+
next: (_res_data_attributes = res.data["@attributes"]) === null || _res_data_attributes === void 0 ? void 0 : _res_data_attributes.next,
|
|
338
|
+
previous: (_res_data_attributes1 = res.data["@attributes"]) === null || _res_data_attributes1 === void 0 ? void 0 : _res_data_attributes1.prev
|
|
339
|
+
};
|
|
340
|
+
} else {
|
|
341
|
+
return res.data;
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
var _err_response;
|
|
345
|
+
// Handle 401 auth errors with automatic retry
|
|
346
|
+
if (((_err_response = err.response) === null || _err_response === void 0 ? void 0 : _err_response.status) === 401 && !options._authRetryAttempted) {
|
|
347
|
+
console.log("🔄 401 error - forcing token refresh and retrying...");
|
|
348
|
+
options._authRetryAttempted = true;
|
|
349
|
+
this.token = null;
|
|
350
|
+
try {
|
|
351
|
+
await this.refreshTokens();
|
|
352
|
+
return this.executeApiRequest(options, retries);
|
|
353
|
+
} catch (refreshError) {
|
|
354
|
+
console.error("Failed to refresh tokens:", refreshError.message);
|
|
355
|
+
throw refreshError;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Handle retryable errors
|
|
359
|
+
if (this.isRetryableError(err) && retries < this.maxRetries) {
|
|
360
|
+
this.handleError(`Network Error Retrying in 2 seconds...`, err.message, false);
|
|
361
|
+
await sleep(2000);
|
|
362
|
+
return this.executeApiRequest(options, retries + 1);
|
|
363
|
+
} else {
|
|
364
|
+
// Simple error handling - let the calling method decide how to handle it
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
294
369
|
// Paginated data fetching
|
|
295
370
|
async getAllData(options) {
|
|
296
371
|
var _options_params;
|
|
@@ -320,6 +320,97 @@ export class LightspeedSDKCore {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
// Core API request handler
|
|
324
|
+
async executeApiRequest(options, retries = 0) {
|
|
325
|
+
await this.handleRateLimit(options);
|
|
326
|
+
|
|
327
|
+
const token = await this.getToken();
|
|
328
|
+
if (!token) throw new Error("Error Fetching Token");
|
|
329
|
+
|
|
330
|
+
options.headers = {
|
|
331
|
+
Authorization: `Bearer ${token}`,
|
|
332
|
+
"Content-Type": "application/json",
|
|
333
|
+
...options.headers,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Centralized query param handling
|
|
337
|
+
if (options.params) {
|
|
338
|
+
const queryString = buildQueryParams(options.params);
|
|
339
|
+
if (queryString) {
|
|
340
|
+
// Remove any trailing ? or & from url
|
|
341
|
+
options.url = options.url.replace(/[?&]+$/, "");
|
|
342
|
+
options.url += (options.url.includes("?") ? "&" : "?") + queryString;
|
|
343
|
+
}
|
|
344
|
+
delete options.params; // Don't let axios try to re-encode
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const res = await axios(options);
|
|
349
|
+
this.lastResponse = res;
|
|
350
|
+
|
|
351
|
+
if (options.method === "GET") {
|
|
352
|
+
// Handle successful response with no data or empty data
|
|
353
|
+
if (!res.data || Object.keys(res.data).length === 0) {
|
|
354
|
+
return {
|
|
355
|
+
data: {},
|
|
356
|
+
next: null,
|
|
357
|
+
previous: null,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check if response has the expected structure but with empty arrays
|
|
362
|
+
const dataKeys = Object.keys(res.data).filter(
|
|
363
|
+
(key) => key !== "@attributes"
|
|
364
|
+
);
|
|
365
|
+
if (dataKeys.length > 0) {
|
|
366
|
+
const firstDataKey = dataKeys[0];
|
|
367
|
+
const firstDataValue = res.data[firstDataKey];
|
|
368
|
+
|
|
369
|
+
// No need to log for empty arrays - this is normal
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Handle successful response with data
|
|
373
|
+
return {
|
|
374
|
+
data: res.data,
|
|
375
|
+
next: res.data["@attributes"]?.next,
|
|
376
|
+
previous: res.data["@attributes"]?.prev,
|
|
377
|
+
};
|
|
378
|
+
} else {
|
|
379
|
+
return res.data;
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
// Handle 401 auth errors with automatic retry
|
|
383
|
+
if (err.response?.status === 401 && !options._authRetryAttempted) {
|
|
384
|
+
console.log("🔄 401 error - forcing token refresh and retrying...");
|
|
385
|
+
|
|
386
|
+
options._authRetryAttempted = true;
|
|
387
|
+
this.token = null;
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
await this.refreshTokens();
|
|
391
|
+
return this.executeApiRequest(options, retries);
|
|
392
|
+
} catch (refreshError) {
|
|
393
|
+
console.error("Failed to refresh tokens:", refreshError.message);
|
|
394
|
+
throw refreshError;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Handle retryable errors
|
|
399
|
+
if (this.isRetryableError(err) && retries < this.maxRetries) {
|
|
400
|
+
this.handleError(
|
|
401
|
+
`Network Error Retrying in 2 seconds...`,
|
|
402
|
+
err.message,
|
|
403
|
+
false
|
|
404
|
+
);
|
|
405
|
+
await sleep(2000);
|
|
406
|
+
return this.executeApiRequest(options, retries + 1);
|
|
407
|
+
} else {
|
|
408
|
+
// Simple error handling - let the calling method decide how to handle it
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
323
414
|
// Paginated data fetching
|
|
324
415
|
async getAllData(options) {
|
|
325
416
|
let allData = [];
|