lightspeed-retail-sdk 3.3.4 → 3.4.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 +64 -8
- package/dist/src/bin/cli.js +215 -51
- 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,16 @@
|
|
|
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.
|
|
5
|
+
**Current Version: 3.4.0** — Enhanced CLI with interactive token injection and production environment support.
|
|
6
6
|
|
|
7
|
-
## **🆕 Recent Updates (v3.
|
|
7
|
+
## **🆕 Recent Updates (v3.4.0)**
|
|
8
|
+
|
|
9
|
+
- **🔧 Enhanced Token Injection**: Interactive `inject-tokens` command with prompts for access/refresh tokens, expiry settings, and storage backend selection
|
|
10
|
+
- **🏭 Production Environment Support**: Login command now supports headless environments with `--no-browser` option and automatic detection
|
|
11
|
+
- **💡 Improved CLI UX**: Better error messages, token validation, and clear instructions for manual OAuth flows
|
|
12
|
+
- **📖 Enhanced Documentation**: Updated README and CLI help with production deployment guidance
|
|
13
|
+
|
|
14
|
+
## **Previous Updates (v3.3.5)**
|
|
8
15
|
|
|
9
16
|
- **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
17
|
- **🎯 Enhanced Parameter Support**: All main getter methods now support both legacy and new object-based parameters with full backward compatibility
|
|
@@ -66,7 +73,7 @@ const items = await sdk.getItems({
|
|
|
66
73
|
## Table of Contents
|
|
67
74
|
|
|
68
75
|
- [Another Unofficial Lightspeed Retail V3 API SDK](#another-unofficial-lightspeed-retail-v3-api-sdk)
|
|
69
|
-
- [**🆕 Recent Updates (v3.3.
|
|
76
|
+
- [**🆕 Recent Updates (v3.3.5)**](#-recent-updates-v335)
|
|
70
77
|
- [🚀 Key Features](#-key-features)
|
|
71
78
|
- [🔄 Migrating from 3.1.x](#-migrating-from-31x)
|
|
72
79
|
- [Backward Compatibility](#backward-compatibility)
|
|
@@ -107,6 +114,8 @@ const items = await sdk.getItems({
|
|
|
107
114
|
- [Alternative: Local Installation](#alternative-local-installation)
|
|
108
115
|
- [Basic Usage (In-Memory Storage)](#basic-usage-in-memory-storage)
|
|
109
116
|
- [Manual Token Management (Advanced)](#manual-token-management-advanced)
|
|
117
|
+
- [Option 1: Interactive Token Injection (Easiest)](#option-1-interactive-token-injection-easiest)
|
|
118
|
+
- [Option 2: Programmatic Token Storage](#option-2-programmatic-token-storage)
|
|
110
119
|
- [File-Based Storage](#file-based-storage)
|
|
111
120
|
- [Encrypted Storage (Recommended)](#encrypted-storage-recommended)
|
|
112
121
|
- [Database Storage (PostgreSQL, SQLite, and MongoDB)](#database-storage-postgresql-sqlite-and-mongodb)
|
|
@@ -197,6 +206,9 @@ lightspeed-retail-sdk login --browser firefox
|
|
|
197
206
|
lightspeed-retail-sdk login --browser "google chrome"
|
|
198
207
|
lightspeed-retail-sdk login --browser safari
|
|
199
208
|
|
|
209
|
+
# Production/headless environment - display URL without opening browser
|
|
210
|
+
lightspeed-retail-sdk login --no-browser
|
|
211
|
+
|
|
200
212
|
# Check current token status
|
|
201
213
|
lightspeed-retail-sdk token-status
|
|
202
214
|
|
|
@@ -205,6 +217,12 @@ lightspeed-retail-sdk refresh-token
|
|
|
205
217
|
|
|
206
218
|
# View your account information
|
|
207
219
|
lightspeed-retail-sdk whoami
|
|
220
|
+
|
|
221
|
+
# Manually inject access and refresh tokens (interactive)
|
|
222
|
+
lightspeed-retail-sdk inject-tokens
|
|
223
|
+
|
|
224
|
+
# Manually inject tokens with command-line options
|
|
225
|
+
lightspeed-retail-sdk inject-tokens --access "your_access_token" --refresh "your_refresh_token"
|
|
208
226
|
```
|
|
209
227
|
|
|
210
228
|
#### Storage Management
|
|
@@ -249,10 +267,11 @@ lightspeed-retail-sdk login
|
|
|
249
267
|
The login process:
|
|
250
268
|
|
|
251
269
|
1. Prompts for your Lightspeed credentials (if not in environment)
|
|
252
|
-
2. Optionally lets you choose a specific browser
|
|
253
|
-
3. Opens browser for OAuth authorization
|
|
254
|
-
4.
|
|
255
|
-
5.
|
|
270
|
+
2. Optionally lets you choose a specific browser (or skips browser opening in production)
|
|
271
|
+
3. Opens browser for OAuth authorization (or displays URL for manual access)
|
|
272
|
+
4. Waits for you to paste the authorization code from the redirect URL
|
|
273
|
+
5. Automatically exchanges code for tokens
|
|
274
|
+
6. Stores tokens in your chosen backend
|
|
256
275
|
|
|
257
276
|
**Note**: If no scopes are specified via environment variables or user input, the default scope `employee:all` will be used.
|
|
258
277
|
|
|
@@ -270,6 +289,22 @@ lightspeed-retail-sdk login --browser "google chrome"
|
|
|
270
289
|
# The CLI will ask if you want to choose a specific browser
|
|
271
290
|
```
|
|
272
291
|
|
|
292
|
+
**Production/Headless Environments:**
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
# Skip browser opening and display URL for manual access
|
|
296
|
+
lightspeed-retail-sdk login --no-browser
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
This is useful when:
|
|
300
|
+
|
|
301
|
+
- Running in Docker containers or cloud environments without GUI
|
|
302
|
+
- SSH sessions without X11 forwarding
|
|
303
|
+
- Automated deployment scripts
|
|
304
|
+
- CI/CD pipelines
|
|
305
|
+
|
|
306
|
+
The CLI will automatically detect headless environments and display the OAuth URL instead of trying to open a browser.
|
|
307
|
+
|
|
273
308
|
#### Token Management
|
|
274
309
|
|
|
275
310
|
##### Manual Token Refresh
|
|
@@ -655,7 +690,28 @@ const api = new LightspeedRetailSDK({
|
|
|
655
690
|
|
|
656
691
|
### Manual Token Management (Advanced)
|
|
657
692
|
|
|
658
|
-
If you prefer to handle authentication manually without the CLI:
|
|
693
|
+
If you prefer to handle authentication manually without the CLI, you have several options:
|
|
694
|
+
|
|
695
|
+
#### Option 1: Interactive Token Injection (Easiest)
|
|
696
|
+
|
|
697
|
+
Use the CLI to manually input tokens you've obtained from elsewhere:
|
|
698
|
+
|
|
699
|
+
```bash
|
|
700
|
+
# Interactive prompts for tokens and storage configuration
|
|
701
|
+
lightspeed-retail-sdk inject-tokens
|
|
702
|
+
|
|
703
|
+
# Or specify tokens via command line and use interactive prompts for storage
|
|
704
|
+
lightspeed-retail-sdk inject-tokens --access "your_access_token" --refresh "your_refresh_token"
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
This method will:
|
|
708
|
+
|
|
709
|
+
- Prompt you to enter access and refresh tokens
|
|
710
|
+
- Let you choose expiry settings (default 1 hour, custom date, or seconds)
|
|
711
|
+
- Allow you to select your storage backend (file, encrypted file, or database)
|
|
712
|
+
- Store the tokens securely in your chosen backend
|
|
713
|
+
|
|
714
|
+
#### Option 2: Programmatic Token Storage
|
|
659
715
|
|
|
660
716
|
#### File-Based Storage
|
|
661
717
|
|
package/dist/src/bin/cli.js
CHANGED
|
@@ -61,6 +61,10 @@ program
|
|
|
61
61
|
"-b, --browser <browser>",
|
|
62
62
|
"Specify browser to use (chrome, firefox, safari, edge, etc.)"
|
|
63
63
|
)
|
|
64
|
+
.option(
|
|
65
|
+
"--no-browser",
|
|
66
|
+
"Skip browser opening and display URL for manual access (useful in production/headless environments)"
|
|
67
|
+
)
|
|
64
68
|
.action(async (options) => {
|
|
65
69
|
let storageBackend = null;
|
|
66
70
|
try {
|
|
@@ -89,70 +93,83 @@ program
|
|
|
89
93
|
clientID
|
|
90
94
|
)}&scope=${encodeURIComponent(scopes)}`;
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
// Detect if we're in a headless/production environment
|
|
97
|
+
const isHeadless = !process.env.DISPLAY && !process.env.SSH_CLIENT && process.platform !== 'darwin' && process.platform !== 'win32';
|
|
98
|
+
const skipBrowser = options.noBrowser || isHeadless;
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log(
|
|
100
|
+
if (skipBrowser) {
|
|
101
|
+
console.log("\n🔗 OAuth Authentication Required");
|
|
102
|
+
console.log("\nPlease open the following URL in your browser to authorize the application:");
|
|
103
|
+
console.log(`\n${authUrl}\n`);
|
|
104
|
+
console.log("After authorizing, you will be redirected to your configured redirect URI");
|
|
105
|
+
console.log("with an authorization code in the URL (e.g., ?code=abc123...)");
|
|
99
106
|
} else {
|
|
100
|
-
|
|
101
|
-
const { chooseBrowser } = await inquirer.prompt([
|
|
102
|
-
{
|
|
103
|
-
type: "confirm",
|
|
104
|
-
name: "chooseBrowser",
|
|
105
|
-
message: "Do you want to choose a specific browser?",
|
|
106
|
-
default: false,
|
|
107
|
-
},
|
|
108
|
-
]);
|
|
107
|
+
console.log("\nOpening browser for authentication...");
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
// Open browser with optional browser specification
|
|
110
|
+
const openOptions = {};
|
|
111
|
+
if (options.browser) {
|
|
112
|
+
openOptions.app = { name: options.browser };
|
|
113
|
+
console.log(`Using browser: ${options.browser}`);
|
|
114
|
+
} else {
|
|
115
|
+
// Ask user if they want to choose a specific browser
|
|
116
|
+
const { chooseBrowser } = await inquirer.prompt([
|
|
112
117
|
{
|
|
113
|
-
type: "
|
|
114
|
-
name: "
|
|
115
|
-
message: "
|
|
116
|
-
|
|
117
|
-
{ name: "Default browser", value: null },
|
|
118
|
-
{ name: "Google Chrome", value: "google chrome" },
|
|
119
|
-
{ name: "Firefox", value: "firefox" },
|
|
120
|
-
{ name: "Safari", value: "safari" },
|
|
121
|
-
{ name: "Microsoft Edge", value: "microsoft edge" },
|
|
122
|
-
{ name: "Brave", value: "brave" },
|
|
123
|
-
{ name: "Opera", value: "opera" },
|
|
124
|
-
{ name: "Custom browser name", value: "custom" },
|
|
125
|
-
],
|
|
118
|
+
type: "confirm",
|
|
119
|
+
name: "chooseBrowser",
|
|
120
|
+
message: "Do you want to choose a specific browser?",
|
|
121
|
+
default: false,
|
|
126
122
|
},
|
|
127
123
|
]);
|
|
128
124
|
|
|
129
|
-
if (
|
|
130
|
-
const {
|
|
125
|
+
if (chooseBrowser) {
|
|
126
|
+
const { browserChoice } = await inquirer.prompt([
|
|
131
127
|
{
|
|
132
|
-
type: "
|
|
133
|
-
name: "
|
|
134
|
-
message: "
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
type: "list",
|
|
129
|
+
name: "browserChoice",
|
|
130
|
+
message: "Select browser:",
|
|
131
|
+
choices: [
|
|
132
|
+
{ name: "Default browser", value: null },
|
|
133
|
+
{ name: "Google Chrome", value: "google chrome" },
|
|
134
|
+
{ name: "Firefox", value: "firefox" },
|
|
135
|
+
{ name: "Safari", value: "safari" },
|
|
136
|
+
{ name: "Microsoft Edge", value: "microsoft edge" },
|
|
137
|
+
{ name: "Brave", value: "brave" },
|
|
138
|
+
{ name: "Opera", value: "opera" },
|
|
139
|
+
{ name: "Custom browser name", value: "custom" },
|
|
140
|
+
],
|
|
137
141
|
},
|
|
138
142
|
]);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
|
|
144
|
+
if (browserChoice === "custom") {
|
|
145
|
+
const { customBrowser } = await inquirer.prompt([
|
|
146
|
+
{
|
|
147
|
+
type: "input",
|
|
148
|
+
name: "customBrowser",
|
|
149
|
+
message: "Enter browser name:",
|
|
150
|
+
validate: (input) =>
|
|
151
|
+
input.trim() !== "" || "Browser name cannot be empty",
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
openOptions.app = { name: customBrowser };
|
|
155
|
+
console.log(`Using browser: ${customBrowser}`);
|
|
156
|
+
} else if (browserChoice) {
|
|
157
|
+
openOptions.app = { name: browserChoice };
|
|
158
|
+
console.log(`Using browser: ${browserChoice}`);
|
|
159
|
+
}
|
|
144
160
|
}
|
|
145
161
|
}
|
|
146
|
-
}
|
|
147
162
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
try {
|
|
164
|
+
await open(authUrl, openOptions);
|
|
165
|
+
console.log(`\nIf the browser didn't open, manually visit: ${authUrl}`);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.log(
|
|
168
|
+
`\n⚠️ Could not open browser automatically: ${err.message}`
|
|
169
|
+
);
|
|
170
|
+
console.log(`\nPlease manually open this URL in your browser:`);
|
|
171
|
+
console.log(`\n${authUrl}\n`);
|
|
172
|
+
}
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
// 3. Prompt for code
|
|
@@ -422,6 +439,153 @@ program
|
|
|
422
439
|
}
|
|
423
440
|
});
|
|
424
441
|
|
|
442
|
+
program
|
|
443
|
+
.command("inject-tokens")
|
|
444
|
+
.description("Manually store an access & refresh token in chosen backend")
|
|
445
|
+
.option("--access <accessToken>", "Access token (skip interactive prompt)")
|
|
446
|
+
.option("--refresh <refreshToken>", "Refresh token (skip interactive prompt)")
|
|
447
|
+
.option(
|
|
448
|
+
"--expires-at <isoDatetime>",
|
|
449
|
+
"ISO8601 expiry (e.g. 2025-01-01T12:34:56.000Z). Overrides --expires-in."
|
|
450
|
+
)
|
|
451
|
+
.option(
|
|
452
|
+
"--expires-in <seconds>",
|
|
453
|
+
"Seconds until expiry (default 3600 if neither --expires-at nor --expires-in provided)"
|
|
454
|
+
)
|
|
455
|
+
.action(async (opts) => {
|
|
456
|
+
let storageBackend = null;
|
|
457
|
+
try {
|
|
458
|
+
console.log("\n🔑 Manual Token Injection\n");
|
|
459
|
+
console.log("This command allows you to manually store access and refresh tokens");
|
|
460
|
+
console.log("that you've obtained through other means (e.g., Lightspeed dashboard).\n");
|
|
461
|
+
|
|
462
|
+
// Get tokens either from command line options or interactive prompts
|
|
463
|
+
let accessToken = opts.access;
|
|
464
|
+
let refreshToken = opts.refresh;
|
|
465
|
+
|
|
466
|
+
if (!accessToken) {
|
|
467
|
+
accessToken = await prompt("Enter your access token: ");
|
|
468
|
+
if (!accessToken || accessToken.trim() === "") {
|
|
469
|
+
console.error("❌ Access token is required");
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
accessToken = accessToken.trim();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!refreshToken) {
|
|
476
|
+
refreshToken = await prompt("Enter your refresh token: ");
|
|
477
|
+
if (!refreshToken || refreshToken.trim() === "") {
|
|
478
|
+
console.error("❌ Refresh token is required");
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
refreshToken = refreshToken.trim();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Get expiry information
|
|
485
|
+
let expiresAt;
|
|
486
|
+
if (opts.expiresAt) {
|
|
487
|
+
const d = new Date(opts.expiresAt);
|
|
488
|
+
if (isNaN(d.getTime())) {
|
|
489
|
+
console.error("❌ Invalid --expires-at value");
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
expiresAt = d.toISOString();
|
|
493
|
+
} else if (opts.expiresIn) {
|
|
494
|
+
const seconds = parseInt(opts.expiresIn, 10);
|
|
495
|
+
if (isNaN(seconds) || seconds <= 0) {
|
|
496
|
+
console.error("❌ Invalid --expires-in value");
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
expiresAt = new Date(Date.now() + seconds * 1000).toISOString();
|
|
500
|
+
} else {
|
|
501
|
+
// Interactive prompt for expiry
|
|
502
|
+
const expiryChoice = await inquirer.prompt([
|
|
503
|
+
{
|
|
504
|
+
type: "list",
|
|
505
|
+
name: "expiryMethod",
|
|
506
|
+
message: "How would you like to set the token expiry?",
|
|
507
|
+
choices: [
|
|
508
|
+
{ name: "Default (1 hour from now)", value: "default" },
|
|
509
|
+
{ name: "Specific date/time (ISO format)", value: "datetime" },
|
|
510
|
+
{ name: "Seconds from now", value: "seconds" },
|
|
511
|
+
],
|
|
512
|
+
default: "default",
|
|
513
|
+
},
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
switch (expiryChoice.expiryMethod) {
|
|
517
|
+
case "default":
|
|
518
|
+
expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
|
|
519
|
+
break;
|
|
520
|
+
case "datetime":
|
|
521
|
+
const { customDateTime } = await inquirer.prompt([
|
|
522
|
+
{
|
|
523
|
+
type: "input",
|
|
524
|
+
name: "customDateTime",
|
|
525
|
+
message: "Enter expiry date/time (ISO format, e.g., 2025-01-01T12:34:56.000Z):",
|
|
526
|
+
validate: (input) => {
|
|
527
|
+
const d = new Date(input);
|
|
528
|
+
return !isNaN(d.getTime()) || "Please enter a valid ISO datetime";
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
]);
|
|
532
|
+
expiresAt = new Date(customDateTime).toISOString();
|
|
533
|
+
break;
|
|
534
|
+
case "seconds":
|
|
535
|
+
const { customSeconds } = await inquirer.prompt([
|
|
536
|
+
{
|
|
537
|
+
type: "input",
|
|
538
|
+
name: "customSeconds",
|
|
539
|
+
message: "Enter seconds until expiry:",
|
|
540
|
+
default: "3600",
|
|
541
|
+
validate: (input) => {
|
|
542
|
+
const num = parseInt(input, 10);
|
|
543
|
+
return (!isNaN(num) && num > 0) || "Please enter a positive number";
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
]);
|
|
547
|
+
expiresAt = new Date(Date.now() + parseInt(customSeconds, 10) * 1000).toISOString();
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log("\n📁 Token Storage Configuration");
|
|
553
|
+
storageBackend = await selectStorageBackend();
|
|
554
|
+
|
|
555
|
+
// Validate tokens format (basic check)
|
|
556
|
+
if (accessToken.length < 10) {
|
|
557
|
+
console.warn("⚠️ Warning: Access token seems unusually short");
|
|
558
|
+
}
|
|
559
|
+
if (refreshToken.length < 10) {
|
|
560
|
+
console.warn("⚠️ Warning: Refresh token seems unusually short");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
await storageBackend.setTokens({
|
|
564
|
+
access_token: accessToken,
|
|
565
|
+
refresh_token: refreshToken,
|
|
566
|
+
expires_at: expiresAt,
|
|
567
|
+
expires_in: Math.floor((new Date(expiresAt) - new Date()) / 1000),
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
console.log("\n✅ Tokens injected successfully!");
|
|
571
|
+
console.log("📋 Token Details:");
|
|
572
|
+
console.log(` Access Token: ${accessToken.substring(0, 20)}...`);
|
|
573
|
+
console.log(` Refresh Token: ${refreshToken.substring(0, 20)}...`);
|
|
574
|
+
console.log(` Expires At: ${expiresAt}`);
|
|
575
|
+
|
|
576
|
+
const minutesUntilExpiry = Math.floor((new Date(expiresAt) - new Date()) / 60000);
|
|
577
|
+
console.log(` Time Until Expiry: ${minutesUntilExpiry} minutes`);
|
|
578
|
+
|
|
579
|
+
console.log("\n💡 You can now use the SDK with these tokens.");
|
|
580
|
+
console.log(" Test with: npm run cli whoami");
|
|
581
|
+
} catch (err) {
|
|
582
|
+
console.error("❌ Failed to inject tokens:", err.message);
|
|
583
|
+
process.exit(1);
|
|
584
|
+
} finally {
|
|
585
|
+
await cleanupStorageBackend(storageBackend);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
425
589
|
program
|
|
426
590
|
.command("migrate-tokens")
|
|
427
591
|
.description(
|
|
@@ -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 = [];
|