lightspeed-retail-sdk 3.3.3 → 3.3.4
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 +45 -85
- package/dist/src/core/LightspeedSDK.mjs +52 -102
- 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.4** — improve error checking on token refresh to prevent false email warnings.
|
|
6
6
|
|
|
7
|
-
## **🆕 Recent Updates (v3.3.
|
|
7
|
+
## **🆕 Recent Updates (v3.3.4)**
|
|
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.4)**](#-recent-updates-v334)
|
|
70
70
|
- [🚀 Key Features](#-key-features)
|
|
71
71
|
- [🔄 Migrating from 3.1.x](#-migrating-from-31x)
|
|
72
72
|
- [Backward Compatibility](#backward-compatibility)
|
|
@@ -207,8 +207,9 @@ class LightspeedSDKCore {
|
|
|
207
207
|
// Token management
|
|
208
208
|
async getToken() {
|
|
209
209
|
const now = new Date();
|
|
210
|
-
const bufferTime = 5 * 60 * 1000; // 5
|
|
210
|
+
const bufferTime = 5 * 60 * 1000; // 5-minute buffer
|
|
211
211
|
const storedTokens = await this.tokenStorage.getTokens();
|
|
212
|
+
// Check if the token is still valid
|
|
212
213
|
if (storedTokens.access_token && storedTokens.expires_at) {
|
|
213
214
|
const expiryTime = new Date(storedTokens.expires_at);
|
|
214
215
|
if (expiryTime.getTime() - now.getTime() > bufferTime) {
|
|
@@ -216,17 +217,35 @@ class LightspeedSDKCore {
|
|
|
216
217
|
return this.token;
|
|
217
218
|
}
|
|
218
219
|
}
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
220
|
+
// Prevent concurrent refresh attempts
|
|
221
|
+
if (this.refreshInProgress) {
|
|
222
|
+
console.log("🔄 Token refresh already in progress. Waiting...");
|
|
223
|
+
while(this.refreshInProgress){
|
|
224
|
+
await sleep(100); // Wait for the ongoing refresh to complete
|
|
225
|
+
}
|
|
226
|
+
// After waiting, check if the token is now valid
|
|
227
|
+
const updatedTokens = await this.tokenStorage.getTokens();
|
|
228
|
+
if (updatedTokens.access_token && updatedTokens.expires_at) {
|
|
229
|
+
const expiryTime = new Date(updatedTokens.expires_at);
|
|
230
|
+
if (expiryTime.getTime() - now.getTime() > bufferTime) {
|
|
231
|
+
this.token = updatedTokens.access_token;
|
|
232
|
+
return this.token;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// If still invalid, proceed to refresh
|
|
222
236
|
}
|
|
223
|
-
|
|
224
|
-
grant_type: "refresh_token",
|
|
225
|
-
client_id: this.clientID,
|
|
226
|
-
client_secret: this.clientSecret,
|
|
227
|
-
refresh_token: refreshToken
|
|
228
|
-
};
|
|
237
|
+
this.refreshInProgress = true; // Lock the refresh process
|
|
229
238
|
try {
|
|
239
|
+
const refreshToken = storedTokens.refresh_token || this.refreshToken;
|
|
240
|
+
if (!refreshToken) {
|
|
241
|
+
throw new Error("No refresh token available");
|
|
242
|
+
}
|
|
243
|
+
const body = {
|
|
244
|
+
grant_type: "refresh_token",
|
|
245
|
+
client_id: this.clientID,
|
|
246
|
+
client_secret: this.clientSecret,
|
|
247
|
+
refresh_token: refreshToken
|
|
248
|
+
};
|
|
230
249
|
const response = await (0, _axios.default)({
|
|
231
250
|
url: this.tokenUrl,
|
|
232
251
|
method: "post",
|
|
@@ -237,6 +256,7 @@ class LightspeedSDKCore {
|
|
|
237
256
|
});
|
|
238
257
|
const tokenData = response.data;
|
|
239
258
|
const expiresAt = new Date(now.getTime() + tokenData.expires_in * 1000);
|
|
259
|
+
// Save the new tokens
|
|
240
260
|
await this.tokenStorage.setTokens({
|
|
241
261
|
access_token: tokenData.access_token,
|
|
242
262
|
refresh_token: tokenData.refresh_token,
|
|
@@ -245,8 +265,20 @@ class LightspeedSDKCore {
|
|
|
245
265
|
});
|
|
246
266
|
this.token = tokenData.access_token;
|
|
247
267
|
this.tokenExpiry = expiresAt;
|
|
268
|
+
console.log("✅ Token refresh successful");
|
|
248
269
|
return this.token;
|
|
249
270
|
} catch (error) {
|
|
271
|
+
console.error("❌ Token refresh failed:", error.message);
|
|
272
|
+
// Check if the token was refreshed by another process
|
|
273
|
+
const updatedTokens = await this.tokenStorage.getTokens();
|
|
274
|
+
if (updatedTokens.access_token && updatedTokens.expires_at) {
|
|
275
|
+
const expiryTime = new Date(updatedTokens.expires_at);
|
|
276
|
+
if (expiryTime.getTime() - now.getTime() > bufferTime) {
|
|
277
|
+
console.log("🔄 Token was refreshed by another process. Skipping failure email.");
|
|
278
|
+
return updatedTokens.access_token;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// If the token is still invalid, send the failure email
|
|
250
282
|
const helpMsg = "\n❌ Token refresh failed. Your refresh token may be expired or revoked.\n" + "👉 Please re-authenticate using the SDK's CLI or obtain a new refresh token.\n" + "Original error: " + ((error === null || error === void 0 ? void 0 : error.message) || error);
|
|
251
283
|
console.error(helpMsg);
|
|
252
284
|
process.emitWarning(helpMsg, {
|
|
@@ -255,81 +287,8 @@ class LightspeedSDKCore {
|
|
|
255
287
|
// Send email notification about token refresh failure
|
|
256
288
|
await sendTokenRefreshFailureEmail(error, this.accountID);
|
|
257
289
|
throw new Error(helpMsg);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Core API request handler
|
|
261
|
-
async executeApiRequest(options, retries = 0) {
|
|
262
|
-
await this.handleRateLimit(options);
|
|
263
|
-
const token = await this.getToken();
|
|
264
|
-
if (!token) throw new Error("Error Fetching Token");
|
|
265
|
-
options.headers = {
|
|
266
|
-
Authorization: `Bearer ${token}`,
|
|
267
|
-
"Content-Type": "application/json",
|
|
268
|
-
...options.headers
|
|
269
|
-
};
|
|
270
|
-
// Centralized query param handling
|
|
271
|
-
if (options.params) {
|
|
272
|
-
const queryString = buildQueryParams(options.params);
|
|
273
|
-
if (queryString) {
|
|
274
|
-
// Remove any trailing ? or & from url
|
|
275
|
-
options.url = options.url.replace(/[?&]+$/, "");
|
|
276
|
-
options.url += (options.url.includes("?") ? "&" : "?") + queryString;
|
|
277
|
-
}
|
|
278
|
-
delete options.params; // Don't let axios try to re-encode
|
|
279
|
-
}
|
|
280
|
-
try {
|
|
281
|
-
const res = await (0, _axios.default)(options);
|
|
282
|
-
this.lastResponse = res;
|
|
283
|
-
if (options.method === "GET") {
|
|
284
|
-
var _res_data_attributes, _res_data_attributes1;
|
|
285
|
-
// Handle successful response with no data or empty data
|
|
286
|
-
if (!res.data || Object.keys(res.data).length === 0) {
|
|
287
|
-
return {
|
|
288
|
-
data: {},
|
|
289
|
-
next: null,
|
|
290
|
-
previous: null
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
// Check if response has the expected structure but with empty arrays
|
|
294
|
-
const dataKeys = Object.keys(res.data).filter((key)=>key !== "@attributes");
|
|
295
|
-
if (dataKeys.length > 0) {
|
|
296
|
-
const firstDataKey = dataKeys[0];
|
|
297
|
-
const firstDataValue = res.data[firstDataKey];
|
|
298
|
-
// No need to log for empty arrays - this is normal
|
|
299
|
-
}
|
|
300
|
-
// Handle successful response with data
|
|
301
|
-
return {
|
|
302
|
-
data: res.data,
|
|
303
|
-
next: (_res_data_attributes = res.data["@attributes"]) === null || _res_data_attributes === void 0 ? void 0 : _res_data_attributes.next,
|
|
304
|
-
previous: (_res_data_attributes1 = res.data["@attributes"]) === null || _res_data_attributes1 === void 0 ? void 0 : _res_data_attributes1.prev
|
|
305
|
-
};
|
|
306
|
-
} else {
|
|
307
|
-
return res.data;
|
|
308
|
-
}
|
|
309
|
-
} catch (err) {
|
|
310
|
-
var _err_response;
|
|
311
|
-
// Handle 401 auth errors with automatic retry
|
|
312
|
-
if (((_err_response = err.response) === null || _err_response === void 0 ? void 0 : _err_response.status) === 401 && !options._authRetryAttempted) {
|
|
313
|
-
console.log("🔄 401 error - forcing token refresh and retrying...");
|
|
314
|
-
options._authRetryAttempted = true;
|
|
315
|
-
this.token = null;
|
|
316
|
-
try {
|
|
317
|
-
await this.refreshTokens();
|
|
318
|
-
return this.executeApiRequest(options, retries);
|
|
319
|
-
} catch (refreshError) {
|
|
320
|
-
console.error("Failed to refresh tokens:", refreshError.message);
|
|
321
|
-
throw refreshError;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
// Handle retryable errors
|
|
325
|
-
if (this.isRetryableError(err) && retries < this.maxRetries) {
|
|
326
|
-
this.handleError(`Network Error Retrying in 2 seconds...`, err.message, false);
|
|
327
|
-
await sleep(2000);
|
|
328
|
-
return this.executeApiRequest(options, retries + 1);
|
|
329
|
-
} else {
|
|
330
|
-
// Simple error handling - let the calling method decide how to handle it
|
|
331
|
-
throw err;
|
|
332
|
-
}
|
|
290
|
+
} finally{
|
|
291
|
+
this.refreshInProgress = false; // Release the lock
|
|
333
292
|
}
|
|
334
293
|
}
|
|
335
294
|
// Paginated data fetching
|
|
@@ -491,6 +450,7 @@ class LightspeedSDKCore {
|
|
|
491
450
|
this.lastResponse = null;
|
|
492
451
|
this.token = null;
|
|
493
452
|
this.tokenExpiry = null;
|
|
453
|
+
this.refreshInProgress = false;
|
|
494
454
|
// Token storage interface - defaults to in-memory if not provided
|
|
495
455
|
this.tokenStorage = tokenStorage || new InMemoryTokenStorage();
|
|
496
456
|
}
|
|
@@ -144,6 +144,7 @@ export class LightspeedSDKCore {
|
|
|
144
144
|
this.lastResponse = null;
|
|
145
145
|
this.token = null;
|
|
146
146
|
this.tokenExpiry = null;
|
|
147
|
+
this.refreshInProgress = false;
|
|
147
148
|
|
|
148
149
|
// Token storage interface - defaults to in-memory if not provided
|
|
149
150
|
this.tokenStorage = tokenStorage || new InMemoryTokenStorage();
|
|
@@ -216,10 +217,11 @@ export class LightspeedSDKCore {
|
|
|
216
217
|
// Token management
|
|
217
218
|
async getToken() {
|
|
218
219
|
const now = new Date();
|
|
219
|
-
const bufferTime = 5 * 60 * 1000; // 5
|
|
220
|
+
const bufferTime = 5 * 60 * 1000; // 5-minute buffer
|
|
220
221
|
|
|
221
222
|
const storedTokens = await this.tokenStorage.getTokens();
|
|
222
223
|
|
|
224
|
+
// Check if the token is still valid
|
|
223
225
|
if (storedTokens.access_token && storedTokens.expires_at) {
|
|
224
226
|
const expiryTime = new Date(storedTokens.expires_at);
|
|
225
227
|
if (expiryTime.getTime() - now.getTime() > bufferTime) {
|
|
@@ -228,20 +230,40 @@ export class LightspeedSDKCore {
|
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
// Prevent concurrent refresh attempts
|
|
234
|
+
if (this.refreshInProgress) {
|
|
235
|
+
console.log("🔄 Token refresh already in progress. Waiting...");
|
|
236
|
+
while (this.refreshInProgress) {
|
|
237
|
+
await sleep(100); // Wait for the ongoing refresh to complete
|
|
238
|
+
}
|
|
239
|
+
// After waiting, check if the token is now valid
|
|
240
|
+
const updatedTokens = await this.tokenStorage.getTokens();
|
|
241
|
+
if (updatedTokens.access_token && updatedTokens.expires_at) {
|
|
242
|
+
const expiryTime = new Date(updatedTokens.expires_at);
|
|
243
|
+
if (expiryTime.getTime() - now.getTime() > bufferTime) {
|
|
244
|
+
this.token = updatedTokens.access_token;
|
|
245
|
+
return this.token;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// If still invalid, proceed to refresh
|
|
235
249
|
}
|
|
236
250
|
|
|
237
|
-
|
|
238
|
-
grant_type: "refresh_token",
|
|
239
|
-
client_id: this.clientID,
|
|
240
|
-
client_secret: this.clientSecret,
|
|
241
|
-
refresh_token: refreshToken,
|
|
242
|
-
};
|
|
251
|
+
this.refreshInProgress = true; // Lock the refresh process
|
|
243
252
|
|
|
244
253
|
try {
|
|
254
|
+
const refreshToken = storedTokens.refresh_token || this.refreshToken;
|
|
255
|
+
|
|
256
|
+
if (!refreshToken) {
|
|
257
|
+
throw new Error("No refresh token available");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const body = {
|
|
261
|
+
grant_type: "refresh_token",
|
|
262
|
+
client_id: this.clientID,
|
|
263
|
+
client_secret: this.clientSecret,
|
|
264
|
+
refresh_token: refreshToken,
|
|
265
|
+
};
|
|
266
|
+
|
|
245
267
|
const response = await axios({
|
|
246
268
|
url: this.tokenUrl,
|
|
247
269
|
method: "post",
|
|
@@ -252,6 +274,7 @@ export class LightspeedSDKCore {
|
|
|
252
274
|
const tokenData = response.data;
|
|
253
275
|
const expiresAt = new Date(now.getTime() + tokenData.expires_in * 1000);
|
|
254
276
|
|
|
277
|
+
// Save the new tokens
|
|
255
278
|
await this.tokenStorage.setTokens({
|
|
256
279
|
access_token: tokenData.access_token,
|
|
257
280
|
refresh_token: tokenData.refresh_token,
|
|
@@ -262,8 +285,24 @@ export class LightspeedSDKCore {
|
|
|
262
285
|
this.token = tokenData.access_token;
|
|
263
286
|
this.tokenExpiry = expiresAt;
|
|
264
287
|
|
|
288
|
+
console.log("✅ Token refresh successful");
|
|
265
289
|
return this.token;
|
|
266
290
|
} catch (error) {
|
|
291
|
+
console.error("❌ Token refresh failed:", error.message);
|
|
292
|
+
|
|
293
|
+
// Check if the token was refreshed by another process
|
|
294
|
+
const updatedTokens = await this.tokenStorage.getTokens();
|
|
295
|
+
if (updatedTokens.access_token && updatedTokens.expires_at) {
|
|
296
|
+
const expiryTime = new Date(updatedTokens.expires_at);
|
|
297
|
+
if (expiryTime.getTime() - now.getTime() > bufferTime) {
|
|
298
|
+
console.log(
|
|
299
|
+
"🔄 Token was refreshed by another process. Skipping failure email."
|
|
300
|
+
);
|
|
301
|
+
return updatedTokens.access_token;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// If the token is still invalid, send the failure email
|
|
267
306
|
const helpMsg =
|
|
268
307
|
"\n❌ Token refresh failed. Your refresh token may be expired or revoked.\n" +
|
|
269
308
|
"👉 Please re-authenticate using the SDK's CLI or obtain a new refresh token.\n" +
|
|
@@ -276,97 +315,8 @@ export class LightspeedSDKCore {
|
|
|
276
315
|
await sendTokenRefreshFailureEmail(error, this.accountID);
|
|
277
316
|
|
|
278
317
|
throw new Error(helpMsg);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// Core API request handler
|
|
283
|
-
async executeApiRequest(options, retries = 0) {
|
|
284
|
-
await this.handleRateLimit(options);
|
|
285
|
-
|
|
286
|
-
const token = await this.getToken();
|
|
287
|
-
if (!token) throw new Error("Error Fetching Token");
|
|
288
|
-
|
|
289
|
-
options.headers = {
|
|
290
|
-
Authorization: `Bearer ${token}`,
|
|
291
|
-
"Content-Type": "application/json",
|
|
292
|
-
...options.headers,
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
// Centralized query param handling
|
|
296
|
-
if (options.params) {
|
|
297
|
-
const queryString = buildQueryParams(options.params);
|
|
298
|
-
if (queryString) {
|
|
299
|
-
// Remove any trailing ? or & from url
|
|
300
|
-
options.url = options.url.replace(/[?&]+$/, "");
|
|
301
|
-
options.url += (options.url.includes("?") ? "&" : "?") + queryString;
|
|
302
|
-
}
|
|
303
|
-
delete options.params; // Don't let axios try to re-encode
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const res = await axios(options);
|
|
308
|
-
this.lastResponse = res;
|
|
309
|
-
|
|
310
|
-
if (options.method === "GET") {
|
|
311
|
-
// Handle successful response with no data or empty data
|
|
312
|
-
if (!res.data || Object.keys(res.data).length === 0) {
|
|
313
|
-
return {
|
|
314
|
-
data: {},
|
|
315
|
-
next: null,
|
|
316
|
-
previous: null,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Check if response has the expected structure but with empty arrays
|
|
321
|
-
const dataKeys = Object.keys(res.data).filter(
|
|
322
|
-
(key) => key !== "@attributes"
|
|
323
|
-
);
|
|
324
|
-
if (dataKeys.length > 0) {
|
|
325
|
-
const firstDataKey = dataKeys[0];
|
|
326
|
-
const firstDataValue = res.data[firstDataKey];
|
|
327
|
-
|
|
328
|
-
// No need to log for empty arrays - this is normal
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Handle successful response with data
|
|
332
|
-
return {
|
|
333
|
-
data: res.data,
|
|
334
|
-
next: res.data["@attributes"]?.next,
|
|
335
|
-
previous: res.data["@attributes"]?.prev,
|
|
336
|
-
};
|
|
337
|
-
} else {
|
|
338
|
-
return res.data;
|
|
339
|
-
}
|
|
340
|
-
} catch (err) {
|
|
341
|
-
// Handle 401 auth errors with automatic retry
|
|
342
|
-
if (err.response?.status === 401 && !options._authRetryAttempted) {
|
|
343
|
-
console.log("🔄 401 error - forcing token refresh and retrying...");
|
|
344
|
-
|
|
345
|
-
options._authRetryAttempted = true;
|
|
346
|
-
this.token = null;
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
await this.refreshTokens();
|
|
350
|
-
return this.executeApiRequest(options, retries);
|
|
351
|
-
} catch (refreshError) {
|
|
352
|
-
console.error("Failed to refresh tokens:", refreshError.message);
|
|
353
|
-
throw refreshError;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Handle retryable errors
|
|
358
|
-
if (this.isRetryableError(err) && retries < this.maxRetries) {
|
|
359
|
-
this.handleError(
|
|
360
|
-
`Network Error Retrying in 2 seconds...`,
|
|
361
|
-
err.message,
|
|
362
|
-
false
|
|
363
|
-
);
|
|
364
|
-
await sleep(2000);
|
|
365
|
-
return this.executeApiRequest(options, retries + 1);
|
|
366
|
-
} else {
|
|
367
|
-
// Simple error handling - let the calling method decide how to handle it
|
|
368
|
-
throw err;
|
|
369
|
-
}
|
|
318
|
+
} finally {
|
|
319
|
+
this.refreshInProgress = false; // Release the lock
|
|
370
320
|
}
|
|
371
321
|
}
|
|
372
322
|
|