lightspeed-retail-sdk 3.3.2 → 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 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.2** — Refactor: extract endpoints for maintainability and improve endpoint testing. Enhanced query param encoding and pagination logic for consistency.
5
+ **Current Version: 3.3.4** — improve error checking on token refresh to prevent false email warnings.
6
6
 
7
- ## **🆕 Recent Updates (v3.3.2)**
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.2)**](#-recent-updates-v332)
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 = 1 * 60 * 1000; // 1 minute buffer
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
- const refreshToken = storedTokens.refresh_token || this.refreshToken;
220
- if (!refreshToken) {
221
- throw new Error("No refresh token available");
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
- const body = {
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 = 1 * 60 * 1000; // 1 minute buffer
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
- const refreshToken = storedTokens.refresh_token || this.refreshToken;
232
-
233
- if (!refreshToken) {
234
- throw new Error("No refresh token available");
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
- const body = {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightspeed-retail-sdk",
3
- "version": "3.3.2",
3
+ "version": "3.3.4",
4
4
  "description": "Another unofficial Lightspeed Retail API SDK for Node.js",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",