@rendomnet/apiservice 1.0.0 → 1.3.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.
- package/README.md +392 -15
- package/dist/AccountManager.d.ts +6 -5
- package/dist/AccountManager.js +6 -5
- package/dist/HookManager.js +11 -11
- package/dist/HttpClient.js +6 -5
- package/dist/index.d.ts +20 -4
- package/dist/index.js +84 -12
- package/dist/types.d.ts +40 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ A robust TypeScript API service framework for making authenticated API calls wit
|
|
|
8
8
|
- ✅ Status code-specific hooks for handling errors
|
|
9
9
|
- ✅ Account state tracking
|
|
10
10
|
- ✅ File upload support
|
|
11
|
+
- ✅ Support for multiple accounts or a single default account
|
|
12
|
+
- ✅ Automatic token refresh for 401 errors
|
|
11
13
|
|
|
12
14
|
## Usage
|
|
13
15
|
|
|
@@ -17,29 +19,353 @@ import ApiService from 'apiservice';
|
|
|
17
19
|
// Create and setup the API service
|
|
18
20
|
const api = new ApiService();
|
|
19
21
|
api.setup({
|
|
20
|
-
provider: 'my-service',
|
|
22
|
+
provider: 'my-service', // 'google' | 'microsoft' and etc.
|
|
21
23
|
tokenService: myTokenService,
|
|
22
24
|
hooks: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
retryDelay: true,
|
|
26
|
-
callback: async (accountId, response) => {
|
|
27
|
-
// Handle token refresh logic
|
|
28
|
-
return { /* updated parameters */ };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
25
|
+
// You can define custom hooks here,
|
|
26
|
+
// or use the default token refresh handler for 401 errors
|
|
31
27
|
},
|
|
32
|
-
cacheTime: 30000 // 30 seconds
|
|
28
|
+
cacheTime: 30000, // 30 seconds
|
|
29
|
+
baseUrl: 'https://api.example.com' // Set default base URL
|
|
33
30
|
});
|
|
34
31
|
|
|
35
|
-
// Make API calls
|
|
36
|
-
const result = await api.
|
|
32
|
+
// Make API calls with specific account ID and use default baseUrl
|
|
33
|
+
const result = await api.call({
|
|
37
34
|
accountId: 'user123',
|
|
38
35
|
method: 'GET',
|
|
39
|
-
base: 'https://api.example.com',
|
|
40
36
|
route: '/users',
|
|
41
|
-
|
|
37
|
+
useAuth: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Override default baseUrl for specific calls
|
|
41
|
+
const customResult = await api.call({
|
|
42
|
+
method: 'GET',
|
|
43
|
+
base: 'https://api2.example.com', // Override default baseUrl
|
|
44
|
+
route: '/users',
|
|
45
|
+
useAuth: true
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Or omit accountId to use the default account ('default')
|
|
49
|
+
const defaultResult = await api.call({
|
|
50
|
+
method: 'GET',
|
|
51
|
+
route: '/users',
|
|
52
|
+
useAuth: true
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Automatic Token Refresh
|
|
57
|
+
|
|
58
|
+
ApiService includes a built-in handler for 401 (Unauthorized) errors that automatically refreshes OAuth tokens. This feature:
|
|
59
|
+
|
|
60
|
+
1. Detects 401 errors from the API
|
|
61
|
+
2. Retrieves the current token for the account
|
|
62
|
+
3. Uses the `refresh` method from your tokenService to obtain a new token
|
|
63
|
+
4. Updates the stored token with the new one
|
|
64
|
+
5. Retries the original API request with the new token
|
|
65
|
+
|
|
66
|
+
To use this feature:
|
|
67
|
+
|
|
68
|
+
1. Ensure your tokenService implements the `refresh` method
|
|
69
|
+
2. Don't specify a custom 401 hook (the default will be used automatically)
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Example token service with refresh capability
|
|
73
|
+
const tokenService = {
|
|
74
|
+
async get(accountId = 'default') {
|
|
75
|
+
// Get token from storage
|
|
76
|
+
return storedToken;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async set(token, accountId = 'default') {
|
|
80
|
+
// Save token to storage
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async refresh(refreshToken, accountId = 'default') {
|
|
84
|
+
// Refresh the token with your OAuth provider
|
|
85
|
+
const response = await fetch('https://api.example.com/oauth/token', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
grant_type: 'refresh_token',
|
|
90
|
+
refresh_token: refreshToken,
|
|
91
|
+
client_id: 'your-client-id'
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error('Failed to refresh token');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return await response.json();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If you prefer to handle token refresh yourself, you can either:
|
|
105
|
+
|
|
106
|
+
1. Provide your own handler for 401 errors which will override the default
|
|
107
|
+
2. Disable the default handler by setting `hooks: { 401: null }`
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Disable default 401 handler without providing a custom one
|
|
111
|
+
api.setup({
|
|
112
|
+
provider: 'my-service',
|
|
113
|
+
tokenService,
|
|
114
|
+
hooks: {
|
|
115
|
+
401: null // Explicitly disable the default handler
|
|
116
|
+
},
|
|
117
|
+
cacheTime: 30000
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Account Management
|
|
122
|
+
|
|
123
|
+
ApiService supports multiple accounts through the `accountId` parameter. This allows you to:
|
|
124
|
+
|
|
125
|
+
1. **Manage multiple tokens** - Maintain separate authentication tokens for different users or services
|
|
126
|
+
2. **Track state by account** - Each account has its own state tracking (request times, failures)
|
|
127
|
+
3. **Apply account-specific retry logic** - Hooks can behave differently based on the account
|
|
128
|
+
|
|
129
|
+
For simple applications that only need a single account, you can omit the accountId parameter:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Make calls without specifying accountId - uses 'default' automatically
|
|
133
|
+
const result = await api.call({
|
|
134
|
+
method: 'GET',
|
|
135
|
+
route: '/users'
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If no accountId is provided, ApiService automatically uses 'default' as the account ID.
|
|
140
|
+
|
|
141
|
+
## Token Service
|
|
142
|
+
|
|
143
|
+
ApiService requires a `tokenService` for authentication. This service manages tokens for different accounts and handles token retrieval, storage, and refresh operations.
|
|
144
|
+
|
|
145
|
+
### TokenService Interface
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
interface TokenService {
|
|
149
|
+
// Get a token for an account (accountId is optional, defaults to 'default')
|
|
150
|
+
get: (accountId?: string) => Promise<Token>;
|
|
151
|
+
|
|
152
|
+
// Save a token for an account
|
|
153
|
+
set: (token: Partial<Token>, accountId?: string) => Promise<void>;
|
|
154
|
+
|
|
155
|
+
// Optional: Refresh an expired token
|
|
156
|
+
refresh?: (refreshToken: string, accountId?: string) => Promise<OAuthToken>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The Token interface
|
|
160
|
+
interface Token {
|
|
161
|
+
accountId: string;
|
|
162
|
+
access_token: string;
|
|
163
|
+
refresh_token: string;
|
|
164
|
+
provider: string;
|
|
165
|
+
enabled?: boolean;
|
|
166
|
+
updatedAt?: string;
|
|
167
|
+
primary?: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// OAuth token response
|
|
171
|
+
interface OAuthToken {
|
|
172
|
+
access_token: string;
|
|
173
|
+
expires_in: number;
|
|
174
|
+
id_token: string;
|
|
175
|
+
refresh_token: string;
|
|
176
|
+
scope: string;
|
|
177
|
+
token_type: string;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Example Implementation
|
|
182
|
+
|
|
183
|
+
Here's a simple `tokenService` implementation using localStorage:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// Simple token service implementation
|
|
187
|
+
const tokenService = {
|
|
188
|
+
// Get token for an account
|
|
189
|
+
async get(accountId = 'default'): Promise<Token> {
|
|
190
|
+
const storedToken = localStorage.getItem(`token-${accountId}`);
|
|
191
|
+
if (!storedToken) {
|
|
192
|
+
throw new Error(`No token found for account ${accountId}`);
|
|
193
|
+
}
|
|
194
|
+
return JSON.parse(storedToken);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Save token for an account
|
|
198
|
+
async set(token: Partial<Token>, accountId = 'default'): Promise<void> {
|
|
199
|
+
const existingToken = localStorage.getItem(`token-${accountId}`);
|
|
200
|
+
const currentToken = existingToken ? JSON.parse(existingToken) : { accountId };
|
|
201
|
+
const updatedToken = { ...currentToken, ...token, updatedAt: new Date().toISOString() };
|
|
202
|
+
localStorage.setItem(`token-${accountId}`, JSON.stringify(updatedToken));
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// Refresh token implementation
|
|
206
|
+
async refresh(refreshToken: string, accountId = 'default'): Promise<OAuthToken> {
|
|
207
|
+
// Make a request to your OAuth token endpoint
|
|
208
|
+
const response = await fetch('https://api.example.com/oauth/token', {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
grant_type: 'refresh_token',
|
|
213
|
+
refresh_token: refreshToken,
|
|
214
|
+
client_id: 'your-client-id'
|
|
215
|
+
})
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error('Failed to refresh token');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const newToken = await response.json();
|
|
223
|
+
|
|
224
|
+
// Update the stored token
|
|
225
|
+
await this.set({
|
|
226
|
+
access_token: newToken.access_token,
|
|
227
|
+
refresh_token: newToken.refresh_token
|
|
228
|
+
}, accountId);
|
|
229
|
+
|
|
230
|
+
return newToken;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Complete Authorization Flow Example
|
|
236
|
+
|
|
237
|
+
Here's a complete example showing how to use ApiService with automatic token refresh:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import ApiService from 'apiservice';
|
|
241
|
+
|
|
242
|
+
// Create token service with refresh capability
|
|
243
|
+
const tokenService = {
|
|
244
|
+
// Get token from storage
|
|
245
|
+
async get(accountId = 'default') {
|
|
246
|
+
const storedToken = localStorage.getItem(`token-${accountId}`);
|
|
247
|
+
if (!storedToken) {
|
|
248
|
+
throw new Error(`No token found for account ${accountId}`);
|
|
249
|
+
}
|
|
250
|
+
return JSON.parse(storedToken);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
// Save token to storage
|
|
254
|
+
async set(token, accountId = 'default') {
|
|
255
|
+
const existingToken = localStorage.getItem(`token-${accountId}`);
|
|
256
|
+
const currentToken = existingToken ? JSON.parse(existingToken) : { accountId };
|
|
257
|
+
const updatedToken = { ...currentToken, ...token, updatedAt: new Date().toISOString() };
|
|
258
|
+
localStorage.setItem(`token-${accountId}`, JSON.stringify(updatedToken));
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// Refresh token with OAuth provider
|
|
262
|
+
async refresh(refreshToken, accountId = 'default') {
|
|
263
|
+
// Real implementation would call your OAuth endpoint
|
|
264
|
+
const response = await fetch('https://api.example.com/oauth/token', {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: { 'Content-Type': 'application/json' },
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
grant_type: 'refresh_token',
|
|
269
|
+
refresh_token: refreshToken,
|
|
270
|
+
client_id: 'your-client-id'
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (!response.ok) {
|
|
275
|
+
throw new Error('Failed to refresh token');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return await response.json();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Create API service instance
|
|
283
|
+
const api = new ApiService();
|
|
284
|
+
|
|
285
|
+
// Configure API service with automatic token refresh
|
|
286
|
+
api.setup({
|
|
287
|
+
provider: 'example-api',
|
|
288
|
+
tokenService,
|
|
289
|
+
cacheTime: 30000,
|
|
290
|
+
baseUrl: 'https://api.example.com',
|
|
291
|
+
|
|
292
|
+
// You can still add custom hooks for other status codes
|
|
293
|
+
// The default 401 handler will be used automatically
|
|
294
|
+
hooks: {
|
|
295
|
+
// Handle 403 Forbidden errors - typically for insufficient permissions
|
|
296
|
+
403: {
|
|
297
|
+
shouldRetry: false,
|
|
298
|
+
handler: async (accountId, response) => {
|
|
299
|
+
console.warn('Permission denied:', response);
|
|
300
|
+
// You could trigger a permissions UI here
|
|
301
|
+
window.dispatchEvent(new CustomEvent('permission:required', {
|
|
302
|
+
detail: { accountId, resource: response.resource }
|
|
303
|
+
}));
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
42
308
|
});
|
|
309
|
+
|
|
310
|
+
// Use the API service
|
|
311
|
+
async function fetchUserData(userId) {
|
|
312
|
+
try {
|
|
313
|
+
return await api.call({
|
|
314
|
+
// No accountId needed - will use 'default' automatically
|
|
315
|
+
method: 'GET',
|
|
316
|
+
route: `/users/${userId}`,
|
|
317
|
+
useAuth: true
|
|
318
|
+
});
|
|
319
|
+
} catch (error) {
|
|
320
|
+
// 401 errors with valid refresh tokens will be automatically handled
|
|
321
|
+
// This catch will only trigger for other errors or if refresh fails
|
|
322
|
+
console.error('Failed to fetch user data:', error);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Hook Options
|
|
329
|
+
|
|
330
|
+
Hooks can be configured to handle specific HTTP status codes:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const hooks = {
|
|
334
|
+
401: {
|
|
335
|
+
// Core settings
|
|
336
|
+
shouldRetry: true, // Whether to retry the API call when this hook is triggered
|
|
337
|
+
useRetryDelay: true, // Whether to apply delay between retries
|
|
338
|
+
maxRetries: 3, // Maximum number of retry attempts for this status code (default: 4)
|
|
339
|
+
|
|
340
|
+
// Advanced options
|
|
341
|
+
preventConcurrentCalls: true, // Wait for an existing hook to complete before starting a new one
|
|
342
|
+
// Useful for avoiding duplicate refresh token calls
|
|
343
|
+
|
|
344
|
+
// Handler functions
|
|
345
|
+
handler: async (accountId, response) => {
|
|
346
|
+
// Main handler function called when this status code is encountered
|
|
347
|
+
// Return an object to update the API call parameters for the retry
|
|
348
|
+
return { /* updated parameters */ };
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
onMaxRetriesExceeded: async (accountId, error) => {
|
|
352
|
+
// Called when all retry attempts for this status code have failed
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
onHandlerError: async (accountId, error) => {
|
|
356
|
+
// Called when the handler function throws an error
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
// Delay strategy settings
|
|
360
|
+
delayStrategy: {
|
|
361
|
+
calculate: (attempt, response) => {
|
|
362
|
+
// Custom strategy for calculating delay between retries
|
|
363
|
+
return 1000 * Math.pow(2, attempt - 1); // Exponential backoff
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
maxDelay: 30000 // Maximum delay in milliseconds between retries (default: 60000)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
43
369
|
```
|
|
44
370
|
|
|
45
371
|
## Architecture
|
|
@@ -50,4 +376,55 @@ The codebase is built around a main `ApiService` class that coordinates several
|
|
|
50
376
|
- `CacheManager`: Implements data caching with customizable expiration times
|
|
51
377
|
- `RetryManager`: Manages retry logic with exponential backoff and other delay strategies
|
|
52
378
|
- `HookManager`: Provides a way to hook into specific status codes and handle them
|
|
53
|
-
- `AccountManager`: Tracks account state and handles account-specific data
|
|
379
|
+
- `AccountManager`: Tracks account state and handles account-specific data
|
|
380
|
+
|
|
381
|
+
## Advanced Usage
|
|
382
|
+
|
|
383
|
+
### Multiple API Providers
|
|
384
|
+
|
|
385
|
+
```javascript
|
|
386
|
+
// Configure multiple API providers
|
|
387
|
+
api.setup({
|
|
388
|
+
provider: 'primary-api',
|
|
389
|
+
tokenService: primaryTokenService,
|
|
390
|
+
cacheTime: 30000,
|
|
391
|
+
baseUrl: 'https://api.primary.com'
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
api.setup({
|
|
395
|
+
provider: 'secondary-api',
|
|
396
|
+
tokenService: secondaryTokenService,
|
|
397
|
+
cacheTime: 60000,
|
|
398
|
+
baseUrl: 'https://api.secondary.com'
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Use different providers in API calls
|
|
402
|
+
async function fetchCombinedData() {
|
|
403
|
+
const [primaryData, secondaryData] = await Promise.all([
|
|
404
|
+
api.call({
|
|
405
|
+
provider: 'primary-api',
|
|
406
|
+
method: 'GET',
|
|
407
|
+
route: '/data',
|
|
408
|
+
useAuth: true
|
|
409
|
+
}),
|
|
410
|
+
|
|
411
|
+
api.call({
|
|
412
|
+
provider: 'secondary-api',
|
|
413
|
+
method: 'GET',
|
|
414
|
+
route: '/data',
|
|
415
|
+
useAuth: true
|
|
416
|
+
}),
|
|
417
|
+
|
|
418
|
+
// Override baseUrl for a specific API call
|
|
419
|
+
api.call({
|
|
420
|
+
provider: 'primary-api',
|
|
421
|
+
method: 'GET',
|
|
422
|
+
route: '/special-data',
|
|
423
|
+
useAuth: true,
|
|
424
|
+
base: 'https://special-api.primary.com'
|
|
425
|
+
})
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
return { primaryData, secondaryData };
|
|
429
|
+
}
|
|
430
|
+
```
|
package/dist/AccountManager.d.ts
CHANGED
|
@@ -4,24 +4,25 @@ import { AccountData } from './types';
|
|
|
4
4
|
*/
|
|
5
5
|
export declare class AccountManager {
|
|
6
6
|
private accounts;
|
|
7
|
+
private readonly DEFAULT_ACCOUNT;
|
|
7
8
|
/**
|
|
8
9
|
* Update account data for a specific account
|
|
9
10
|
*/
|
|
10
|
-
updateAccountData(accountId: string, data: Partial<AccountData>): void;
|
|
11
|
+
updateAccountData(accountId: string | undefined, data: Partial<AccountData>): void;
|
|
11
12
|
/**
|
|
12
13
|
* Get account data for a specific account
|
|
13
14
|
*/
|
|
14
|
-
getAccountData(accountId
|
|
15
|
+
getAccountData(accountId?: string): AccountData;
|
|
15
16
|
/**
|
|
16
17
|
* Check if an account's last request failed
|
|
17
18
|
*/
|
|
18
|
-
didLastRequestFail(accountId
|
|
19
|
+
didLastRequestFail(accountId?: string): boolean;
|
|
19
20
|
/**
|
|
20
21
|
* Set account's last request as failed
|
|
21
22
|
*/
|
|
22
|
-
setLastRequestFailed(accountId
|
|
23
|
+
setLastRequestFailed(accountId?: string, failed?: boolean): void;
|
|
23
24
|
/**
|
|
24
25
|
* Update the last request time for an account
|
|
25
26
|
*/
|
|
26
|
-
updateLastRequestTime(accountId
|
|
27
|
+
updateLastRequestTime(accountId?: string): void;
|
|
27
28
|
}
|
package/dist/AccountManager.js
CHANGED
|
@@ -7,11 +7,12 @@ exports.AccountManager = void 0;
|
|
|
7
7
|
class AccountManager {
|
|
8
8
|
constructor() {
|
|
9
9
|
this.accounts = {};
|
|
10
|
+
this.DEFAULT_ACCOUNT = 'default';
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Update account data for a specific account
|
|
13
14
|
*/
|
|
14
|
-
updateAccountData(accountId, data) {
|
|
15
|
+
updateAccountData(accountId = this.DEFAULT_ACCOUNT, data) {
|
|
15
16
|
this.accounts[accountId] = {
|
|
16
17
|
...this.accounts[accountId],
|
|
17
18
|
...data
|
|
@@ -20,26 +21,26 @@ class AccountManager {
|
|
|
20
21
|
/**
|
|
21
22
|
* Get account data for a specific account
|
|
22
23
|
*/
|
|
23
|
-
getAccountData(accountId) {
|
|
24
|
+
getAccountData(accountId = this.DEFAULT_ACCOUNT) {
|
|
24
25
|
return this.accounts[accountId] || {};
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Check if an account's last request failed
|
|
28
29
|
*/
|
|
29
|
-
didLastRequestFail(accountId) {
|
|
30
|
+
didLastRequestFail(accountId = this.DEFAULT_ACCOUNT) {
|
|
30
31
|
var _a;
|
|
31
32
|
return !!((_a = this.accounts[accountId]) === null || _a === void 0 ? void 0 : _a.lastFailed);
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* Set account's last request as failed
|
|
35
36
|
*/
|
|
36
|
-
setLastRequestFailed(accountId, failed = true) {
|
|
37
|
+
setLastRequestFailed(accountId = this.DEFAULT_ACCOUNT, failed = true) {
|
|
37
38
|
this.updateAccountData(accountId, { lastFailed: failed });
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* Update the last request time for an account
|
|
41
42
|
*/
|
|
42
|
-
updateLastRequestTime(accountId) {
|
|
43
|
+
updateLastRequestTime(accountId = this.DEFAULT_ACCOUNT) {
|
|
43
44
|
this.updateAccountData(accountId, { lastRequestTime: Date.now() });
|
|
44
45
|
}
|
|
45
46
|
}
|
package/dist/HookManager.js
CHANGED
|
@@ -26,26 +26,26 @@ class HookManager {
|
|
|
26
26
|
*/
|
|
27
27
|
async processHook(accountId, status, error) {
|
|
28
28
|
const hook = this.hooks[status];
|
|
29
|
-
if (!hook || !hook.
|
|
29
|
+
if (!hook || !hook.handler)
|
|
30
30
|
return null;
|
|
31
|
-
const hookKey = `${accountId}-${status}`;
|
|
31
|
+
const hookKey = `${accountId || 'default'}-${status}`;
|
|
32
32
|
try {
|
|
33
33
|
// Handle waiting for existing hook call if needed
|
|
34
|
-
if (hook.
|
|
34
|
+
if (hook.preventConcurrentCalls) {
|
|
35
35
|
if (!this.hookPromises[hookKey]) {
|
|
36
|
-
this.hookPromises[hookKey] = Promise.resolve(hook.
|
|
36
|
+
this.hookPromises[hookKey] = Promise.resolve(hook.handler(accountId, error.response) || {});
|
|
37
37
|
}
|
|
38
38
|
const result = await this.hookPromises[hookKey];
|
|
39
39
|
delete this.hookPromises[hookKey];
|
|
40
40
|
return result;
|
|
41
41
|
}
|
|
42
42
|
// Otherwise just call the hook directly
|
|
43
|
-
return await hook.
|
|
43
|
+
return await hook.handler(accountId, error.response) || {};
|
|
44
44
|
}
|
|
45
45
|
catch (hookError) {
|
|
46
|
-
console.error(`Hook
|
|
47
|
-
if (hook.
|
|
48
|
-
await hook.
|
|
46
|
+
console.error(`Hook handler failed for status ${status}:`, hookError);
|
|
47
|
+
if (hook.onHandlerError) {
|
|
48
|
+
await hook.onHandlerError(accountId, hookError);
|
|
49
49
|
}
|
|
50
50
|
throw hookError;
|
|
51
51
|
}
|
|
@@ -55,8 +55,8 @@ class HookManager {
|
|
|
55
55
|
*/
|
|
56
56
|
async handleRetryFailure(accountId, status, error) {
|
|
57
57
|
const hook = this.hooks[status];
|
|
58
|
-
if (hook === null || hook === void 0 ? void 0 : hook.
|
|
59
|
-
await hook.
|
|
58
|
+
if (hook === null || hook === void 0 ? void 0 : hook.onMaxRetriesExceeded) {
|
|
59
|
+
await hook.onMaxRetriesExceeded(accountId, error);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
@@ -64,7 +64,7 @@ class HookManager {
|
|
|
64
64
|
*/
|
|
65
65
|
shouldRetry(status) {
|
|
66
66
|
const hook = this.hooks[status];
|
|
67
|
-
return !!hook && !!hook.
|
|
67
|
+
return !!hook && !!hook.shouldRetry;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
exports.HookManager = HookManager;
|
package/dist/HttpClient.js
CHANGED
|
@@ -15,7 +15,7 @@ class HttpClient {
|
|
|
15
15
|
* Make an HTTP request
|
|
16
16
|
*/
|
|
17
17
|
async makeRequest(apiParams, authToken) {
|
|
18
|
-
const { accountId, method, route, base, body, data, headers, queryParams, contentType = 'application/json', accessToken: forcedAccessToken,
|
|
18
|
+
const { accountId, method, route, base, body, data, headers, queryParams, contentType = 'application/json', accessToken: forcedAccessToken, useAuth = true, files, } = apiParams;
|
|
19
19
|
// Build URL and request body
|
|
20
20
|
const url = this.buildUrl(base, route, queryParams);
|
|
21
21
|
const requestBody = body || data;
|
|
@@ -30,7 +30,7 @@ class HttpClient {
|
|
|
30
30
|
contentType,
|
|
31
31
|
authToken,
|
|
32
32
|
forcedAccessToken,
|
|
33
|
-
|
|
33
|
+
useAuth,
|
|
34
34
|
headers,
|
|
35
35
|
});
|
|
36
36
|
// Make the request
|
|
@@ -48,7 +48,8 @@ class HttpClient {
|
|
|
48
48
|
* Build URL with query parameters
|
|
49
49
|
*/
|
|
50
50
|
buildUrl(base, route, queryParams) {
|
|
51
|
-
|
|
51
|
+
const baseUrl = base || '';
|
|
52
|
+
let url = `${baseUrl}${route || ''}`;
|
|
52
53
|
if (queryParams)
|
|
53
54
|
url += `?${qs_1.default.stringify(queryParams)}`;
|
|
54
55
|
return url;
|
|
@@ -70,12 +71,12 @@ class HttpClient {
|
|
|
70
71
|
/**
|
|
71
72
|
* Build fetch options for request
|
|
72
73
|
*/
|
|
73
|
-
buildFetchOptions({ method, body, formData, contentType, authToken, forcedAccessToken,
|
|
74
|
+
buildFetchOptions({ method, body, formData, contentType, authToken, forcedAccessToken, useAuth, headers, }) {
|
|
74
75
|
const allowedMethods = ['POST', 'PUT', 'PATCH'];
|
|
75
76
|
return {
|
|
76
77
|
method,
|
|
77
78
|
headers: {
|
|
78
|
-
...(
|
|
79
|
+
...(useAuth && {
|
|
79
80
|
Authorization: `Bearer ${forcedAccessToken || authToken.access_token}`
|
|
80
81
|
}),
|
|
81
82
|
...(!formData && { 'content-type': contentType }),
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { TokenService, ApiCallParams, HookSettings, StatusCode } from './types';
|
|
|
6
6
|
declare class ApiService {
|
|
7
7
|
provider: string;
|
|
8
8
|
private tokenService;
|
|
9
|
+
private baseUrl;
|
|
9
10
|
private cacheManager;
|
|
10
11
|
private retryManager;
|
|
11
12
|
private hookManager;
|
|
@@ -16,12 +17,18 @@ declare class ApiService {
|
|
|
16
17
|
/**
|
|
17
18
|
* Setup the API service
|
|
18
19
|
*/
|
|
19
|
-
setup({ provider, tokenService, hooks, cacheTime, }: {
|
|
20
|
+
setup({ provider, tokenService, hooks, cacheTime, baseUrl, }: {
|
|
20
21
|
provider: string;
|
|
21
22
|
tokenService: TokenService;
|
|
22
|
-
hooks?: Record<StatusCode, HookSettings>;
|
|
23
|
+
hooks?: Record<StatusCode, HookSettings | null>;
|
|
23
24
|
cacheTime: number;
|
|
25
|
+
baseUrl?: string;
|
|
24
26
|
}): void;
|
|
27
|
+
/**
|
|
28
|
+
* Create a default handler for 401 (Unauthorized) errors
|
|
29
|
+
* that implements standard token refresh behavior
|
|
30
|
+
*/
|
|
31
|
+
private createDefaultTokenRefreshHandler;
|
|
25
32
|
/**
|
|
26
33
|
* Set the maximum number of retry attempts
|
|
27
34
|
*/
|
|
@@ -31,9 +38,18 @@ declare class ApiService {
|
|
|
31
38
|
*/
|
|
32
39
|
updateAccountData(accountId: string, data: Partial<Record<string, any>>): void;
|
|
33
40
|
/**
|
|
34
|
-
*
|
|
41
|
+
* Make an API call with all features (caching, retry, hooks)
|
|
42
|
+
*/
|
|
43
|
+
call(apiCallParams: Omit<ApiCallParams, 'accountId'> & {
|
|
44
|
+
accountId?: string;
|
|
45
|
+
}): Promise<any>;
|
|
46
|
+
/**
|
|
47
|
+
* Legacy method for backward compatibility
|
|
48
|
+
* @deprecated Use call() instead
|
|
35
49
|
*/
|
|
36
|
-
makeApiCall(apiCallParams: ApiCallParams
|
|
50
|
+
makeApiCall(apiCallParams: Omit<ApiCallParams, 'accountId'> & {
|
|
51
|
+
accountId?: string;
|
|
52
|
+
}): Promise<any>;
|
|
37
53
|
/**
|
|
38
54
|
* Make a request with retry capability
|
|
39
55
|
*/
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const components_1 = require("./components");
|
|
|
7
7
|
*/
|
|
8
8
|
class ApiService {
|
|
9
9
|
constructor() {
|
|
10
|
+
this.baseUrl = ''; // Default base URL
|
|
10
11
|
// Default max attempts for API calls
|
|
11
12
|
this.maxAttempts = 10;
|
|
12
13
|
this.provider = '';
|
|
@@ -21,16 +22,73 @@ class ApiService {
|
|
|
21
22
|
/**
|
|
22
23
|
* Setup the API service
|
|
23
24
|
*/
|
|
24
|
-
setup({ provider, tokenService, hooks, cacheTime, }) {
|
|
25
|
+
setup({ provider, tokenService, hooks = {}, cacheTime, baseUrl = '', }) {
|
|
25
26
|
this.provider = provider;
|
|
26
27
|
this.tokenService = tokenService;
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
this.baseUrl = baseUrl;
|
|
29
|
+
// Create a copy of hooks to avoid modifying the input
|
|
30
|
+
const finalHooks = {};
|
|
31
|
+
// Apply default 401 handler if:
|
|
32
|
+
// 1. No 401 hook is explicitly defined (or is explicitly null)
|
|
33
|
+
// 2. TokenService has a refresh method
|
|
34
|
+
if (hooks[401] === undefined && typeof this.tokenService.refresh === 'function') {
|
|
35
|
+
finalHooks[401] = this.createDefaultTokenRefreshHandler();
|
|
36
|
+
}
|
|
37
|
+
// Add user-defined hooks (skipping null/undefined values)
|
|
38
|
+
for (const [statusCode, hook] of Object.entries(hooks)) {
|
|
39
|
+
if (hook) {
|
|
40
|
+
finalHooks[statusCode] = hook;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Set the hooks if we have any
|
|
44
|
+
if (Object.keys(finalHooks).length > 0) {
|
|
45
|
+
this.hookManager.setHooks(finalHooks);
|
|
29
46
|
}
|
|
30
47
|
if (typeof cacheTime !== 'undefined') {
|
|
31
48
|
this.cacheManager.setCacheTime(cacheTime);
|
|
32
49
|
}
|
|
33
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a default handler for 401 (Unauthorized) errors
|
|
53
|
+
* that implements standard token refresh behavior
|
|
54
|
+
*/
|
|
55
|
+
createDefaultTokenRefreshHandler() {
|
|
56
|
+
return {
|
|
57
|
+
shouldRetry: true,
|
|
58
|
+
useRetryDelay: true,
|
|
59
|
+
preventConcurrentCalls: true,
|
|
60
|
+
maxRetries: 1,
|
|
61
|
+
handler: async (accountId) => {
|
|
62
|
+
try {
|
|
63
|
+
console.log(`🔄 Using default token refresh handler for ${accountId}`);
|
|
64
|
+
// Get current token to extract refresh token
|
|
65
|
+
const currentToken = await this.tokenService.get(accountId);
|
|
66
|
+
if (!currentToken.refresh_token) {
|
|
67
|
+
throw new Error(`No refresh token available for account ${accountId}`);
|
|
68
|
+
}
|
|
69
|
+
// Refresh the token
|
|
70
|
+
const newToken = await this.tokenService.refresh(currentToken.refresh_token, accountId);
|
|
71
|
+
if (!newToken || !newToken.access_token) {
|
|
72
|
+
throw new Error('Token refresh returned invalid data');
|
|
73
|
+
}
|
|
74
|
+
// Update the token in storage
|
|
75
|
+
await this.tokenService.set({
|
|
76
|
+
access_token: newToken.access_token,
|
|
77
|
+
refresh_token: newToken.refresh_token || currentToken.refresh_token
|
|
78
|
+
}, accountId);
|
|
79
|
+
// Return empty object to retry with same parameters
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error(`Token refresh failed for ${accountId}:`, error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
onMaxRetriesExceeded: async (accountId, error) => {
|
|
88
|
+
console.error(`Authentication failed after refresh attempt for ${accountId}:`, error);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
34
92
|
/**
|
|
35
93
|
* Set the maximum number of retry attempts
|
|
36
94
|
*/
|
|
@@ -44,20 +102,34 @@ class ApiService {
|
|
|
44
102
|
this.accountManager.updateAccountData(accountId, data);
|
|
45
103
|
}
|
|
46
104
|
/**
|
|
47
|
-
*
|
|
105
|
+
* Make an API call with all features (caching, retry, hooks)
|
|
48
106
|
*/
|
|
49
|
-
async
|
|
50
|
-
|
|
107
|
+
async call(apiCallParams) {
|
|
108
|
+
// Use 'default' as fallback if accountId is not provided
|
|
109
|
+
// and use default baseUrl if not provided
|
|
110
|
+
const params = {
|
|
111
|
+
...apiCallParams,
|
|
112
|
+
accountId: apiCallParams.accountId || 'default',
|
|
113
|
+
base: apiCallParams.base || this.baseUrl,
|
|
114
|
+
};
|
|
115
|
+
console.log('🔄 API call', this.provider, params.accountId, params.method, params.route);
|
|
51
116
|
// Check cache first
|
|
52
|
-
const cachedData = this.cacheManager.getFromCache(
|
|
117
|
+
const cachedData = this.cacheManager.getFromCache(params);
|
|
53
118
|
if (cachedData)
|
|
54
119
|
return cachedData;
|
|
55
120
|
// Make the API call with retry capability
|
|
56
|
-
const result = await this.makeRequestWithRetry(
|
|
121
|
+
const result = await this.makeRequestWithRetry(params);
|
|
57
122
|
// Cache the result
|
|
58
|
-
this.cacheManager.saveToCache(
|
|
123
|
+
this.cacheManager.saveToCache(params, result);
|
|
59
124
|
return result;
|
|
60
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Legacy method for backward compatibility
|
|
128
|
+
* @deprecated Use call() instead
|
|
129
|
+
*/
|
|
130
|
+
async makeApiCall(apiCallParams) {
|
|
131
|
+
return this.call(apiCallParams);
|
|
132
|
+
}
|
|
61
133
|
/**
|
|
62
134
|
* Make a request with retry capability
|
|
63
135
|
*/
|
|
@@ -73,11 +145,11 @@ class ApiService {
|
|
|
73
145
|
attempts++;
|
|
74
146
|
try {
|
|
75
147
|
// Get authentication token if needed
|
|
76
|
-
const authToken = apiCallParams.
|
|
148
|
+
const authToken = apiCallParams.useAuth !== false
|
|
77
149
|
? await this.tokenService.get(accountId)
|
|
78
150
|
: {};
|
|
79
151
|
// Verify we have authentication if required
|
|
80
|
-
if (apiCallParams.
|
|
152
|
+
if (apiCallParams.useAuth !== false && !apiCallParams.accessToken && !authToken.access_token) {
|
|
81
153
|
throw new Error(`${this.provider} credentials not found for account ID ${accountId}`);
|
|
82
154
|
}
|
|
83
155
|
// Make the actual API call
|
|
@@ -114,7 +186,7 @@ class ApiService {
|
|
|
114
186
|
throw hookError;
|
|
115
187
|
}
|
|
116
188
|
// Wait before retrying if needed
|
|
117
|
-
if (activeHook === null || activeHook === void 0 ? void 0 : activeHook.
|
|
189
|
+
if (activeHook === null || activeHook === void 0 ? void 0 : activeHook.useRetryDelay) {
|
|
118
190
|
await this.retryManager.calculateAndDelay({
|
|
119
191
|
attempt: statusRetries[status],
|
|
120
192
|
response: error.response,
|
package/dist/types.d.ts
CHANGED
|
@@ -27,33 +27,62 @@ interface ApiCallParams {
|
|
|
27
27
|
accountId: string;
|
|
28
28
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
29
29
|
route: string;
|
|
30
|
-
base
|
|
30
|
+
base?: string;
|
|
31
31
|
body?: object;
|
|
32
32
|
data?: object;
|
|
33
33
|
headers?: Record<string, string>;
|
|
34
34
|
queryParams?: URLSearchParams;
|
|
35
35
|
accessToken?: string;
|
|
36
|
-
|
|
36
|
+
useAuth?: boolean;
|
|
37
37
|
noContentType?: boolean;
|
|
38
38
|
contentType?: string;
|
|
39
39
|
cacheTime?: number;
|
|
40
40
|
files?: File[];
|
|
41
41
|
}
|
|
42
42
|
interface HookSettings {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Whether to retry the API call when this hook is triggered
|
|
45
|
+
*/
|
|
46
|
+
shouldRetry: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether to apply delay between retries
|
|
49
|
+
*/
|
|
50
|
+
useRetryDelay: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The maximum number of retry attempts for this status code
|
|
53
|
+
*/
|
|
46
54
|
maxRetries?: number;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Wait for an existing hook to complete before starting a new one
|
|
57
|
+
* Useful for avoiding duplicate refresh token calls
|
|
58
|
+
*/
|
|
59
|
+
preventConcurrentCalls?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The main handler function called when this status code is encountered
|
|
62
|
+
* Return an object to update the API call parameters for the retry
|
|
63
|
+
*/
|
|
64
|
+
handler: (accountId: string, response: any) => Promise<any>;
|
|
65
|
+
/**
|
|
66
|
+
* Called when all retry attempts for this status code have failed
|
|
67
|
+
*/
|
|
68
|
+
onMaxRetriesExceeded?: (accountId: string, error: any) => Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Called when the handler function throws an error
|
|
71
|
+
*/
|
|
72
|
+
onHandlerError?: (accountId: string, error: any) => Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Custom strategy for calculating delay between retries
|
|
75
|
+
*/
|
|
50
76
|
delayStrategy?: DelayStrategy;
|
|
77
|
+
/**
|
|
78
|
+
* Maximum delay in milliseconds between retries
|
|
79
|
+
*/
|
|
51
80
|
maxDelay?: number;
|
|
52
81
|
}
|
|
53
82
|
type StatusCode = string | number;
|
|
54
83
|
type TokenService = {
|
|
55
|
-
get: (accountId
|
|
56
|
-
set: (
|
|
57
|
-
refresh?: (
|
|
84
|
+
get: (accountId?: string) => Promise<Token>;
|
|
85
|
+
set: (token: Partial<Token>, accountId?: string) => Promise<void>;
|
|
86
|
+
refresh?: (refreshToken: string, accountId?: string) => Promise<OAuthToken>;
|
|
58
87
|
};
|
|
59
88
|
export { OAuthToken, DelayStrategy, Token, AccountData, ApiCallParams, HookSettings, StatusCode, TokenService, };
|