@rendomnet/apiservice 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rendom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # ApiService
2
+
3
+ A robust TypeScript API service framework for making authenticated API calls with advanced features:
4
+
5
+ - ✅ Authentication handling with token management
6
+ - ✅ Request caching with configurable time periods
7
+ - ✅ Advanced retry mechanisms with exponential backoff
8
+ - ✅ Status code-specific hooks for handling errors
9
+ - ✅ Account state tracking
10
+ - ✅ File upload support
11
+
12
+ ## Usage
13
+
14
+ ```typescript
15
+ import ApiService from 'apiservice';
16
+
17
+ // Create and setup the API service
18
+ const api = new ApiService();
19
+ api.setup({
20
+ provider: 'my-service',
21
+ tokenService: myTokenService,
22
+ hooks: {
23
+ 401: {
24
+ retryCall: true,
25
+ retryDelay: true,
26
+ callback: async (accountId, response) => {
27
+ // Handle token refresh logic
28
+ return { /* updated parameters */ };
29
+ }
30
+ }
31
+ },
32
+ cacheTime: 30000 // 30 seconds
33
+ });
34
+
35
+ // Make API calls
36
+ const result = await api.makeApiCall({
37
+ accountId: 'user123',
38
+ method: 'GET',
39
+ base: 'https://api.example.com',
40
+ route: '/users',
41
+ requireAuth: true
42
+ });
43
+ ```
44
+
45
+ ## Architecture
46
+
47
+ The codebase is built around a main `ApiService` class that coordinates several component managers:
48
+
49
+ - `HttpClient`: Handles the actual HTTP request creation and execution
50
+ - `CacheManager`: Implements data caching with customizable expiration times
51
+ - `RetryManager`: Manages retry logic with exponential backoff and other delay strategies
52
+ - `HookManager`: Provides a way to hook into specific status codes and handle them
53
+ - `AccountManager`: Tracks account state and handles account-specific data
@@ -0,0 +1,27 @@
1
+ import { AccountData } from './types';
2
+ /**
3
+ * Manages account data and state
4
+ */
5
+ export declare class AccountManager {
6
+ private accounts;
7
+ /**
8
+ * Update account data for a specific account
9
+ */
10
+ updateAccountData(accountId: string, data: Partial<AccountData>): void;
11
+ /**
12
+ * Get account data for a specific account
13
+ */
14
+ getAccountData(accountId: string): AccountData;
15
+ /**
16
+ * Check if an account's last request failed
17
+ */
18
+ didLastRequestFail(accountId: string): boolean;
19
+ /**
20
+ * Set account's last request as failed
21
+ */
22
+ setLastRequestFailed(accountId: string, failed?: boolean): void;
23
+ /**
24
+ * Update the last request time for an account
25
+ */
26
+ updateLastRequestTime(accountId: string): void;
27
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccountManager = void 0;
4
+ /**
5
+ * Manages account data and state
6
+ */
7
+ class AccountManager {
8
+ constructor() {
9
+ this.accounts = {};
10
+ }
11
+ /**
12
+ * Update account data for a specific account
13
+ */
14
+ updateAccountData(accountId, data) {
15
+ this.accounts[accountId] = {
16
+ ...this.accounts[accountId],
17
+ ...data
18
+ };
19
+ }
20
+ /**
21
+ * Get account data for a specific account
22
+ */
23
+ getAccountData(accountId) {
24
+ return this.accounts[accountId] || {};
25
+ }
26
+ /**
27
+ * Check if an account's last request failed
28
+ */
29
+ didLastRequestFail(accountId) {
30
+ var _a;
31
+ return !!((_a = this.accounts[accountId]) === null || _a === void 0 ? void 0 : _a.lastFailed);
32
+ }
33
+ /**
34
+ * Set account's last request as failed
35
+ */
36
+ setLastRequestFailed(accountId, failed = true) {
37
+ this.updateAccountData(accountId, { lastFailed: failed });
38
+ }
39
+ /**
40
+ * Update the last request time for an account
41
+ */
42
+ updateLastRequestTime(accountId) {
43
+ this.updateAccountData(accountId, { lastRequestTime: Date.now() });
44
+ }
45
+ }
46
+ exports.AccountManager = AccountManager;
@@ -0,0 +1,28 @@
1
+ import { ApiCallParams } from './types';
2
+ /**
3
+ * Handles caching of API responses
4
+ */
5
+ export declare class CacheManager {
6
+ private cache;
7
+ private cacheTime;
8
+ /**
9
+ * Get data from cache if available and not expired
10
+ */
11
+ getFromCache(apiCallParams: ApiCallParams): any;
12
+ /**
13
+ * Save data to cache
14
+ */
15
+ saveToCache(apiCallParams: ApiCallParams, data: any): void;
16
+ /**
17
+ * Generate a unique key for caching based on request parameters
18
+ */
19
+ private getRequestKey;
20
+ /**
21
+ * Set the default cache time in milliseconds
22
+ */
23
+ setCacheTime(milliseconds: number): void;
24
+ /**
25
+ * Clear the entire cache
26
+ */
27
+ clearCache(): void;
28
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CacheManager = void 0;
4
+ /**
5
+ * Handles caching of API responses
6
+ */
7
+ class CacheManager {
8
+ constructor() {
9
+ this.cache = new Map();
10
+ this.cacheTime = 20000; // Default cache time of 20 seconds
11
+ }
12
+ /**
13
+ * Get data from cache if available and not expired
14
+ */
15
+ getFromCache(apiCallParams) {
16
+ var _a;
17
+ const requestKey = this.getRequestKey(apiCallParams);
18
+ const currentCacheTime = (_a = apiCallParams.cacheTime) !== null && _a !== void 0 ? _a : this.cacheTime;
19
+ const cached = this.cache.get(requestKey);
20
+ if (cached && (Date.now() - cached.timestamp < currentCacheTime)) {
21
+ return cached.data;
22
+ }
23
+ return null;
24
+ }
25
+ /**
26
+ * Save data to cache
27
+ */
28
+ saveToCache(apiCallParams, data) {
29
+ const requestKey = this.getRequestKey(apiCallParams);
30
+ this.cache.set(requestKey, {
31
+ data,
32
+ timestamp: Date.now()
33
+ });
34
+ }
35
+ /**
36
+ * Generate a unique key for caching based on request parameters
37
+ */
38
+ getRequestKey(apiCallParams) {
39
+ const { accountId, method, route, base, queryParams, body, data } = apiCallParams;
40
+ return JSON.stringify({
41
+ accountId,
42
+ method,
43
+ route,
44
+ base,
45
+ queryParams,
46
+ body: body || data,
47
+ });
48
+ }
49
+ /**
50
+ * Set the default cache time in milliseconds
51
+ */
52
+ setCacheTime(milliseconds) {
53
+ this.cacheTime = milliseconds;
54
+ }
55
+ /**
56
+ * Clear the entire cache
57
+ */
58
+ clearCache() {
59
+ this.cache.clear();
60
+ }
61
+ }
62
+ exports.CacheManager = CacheManager;
@@ -0,0 +1,8 @@
1
+ export declare class FetchError extends Error {
2
+ name: string;
3
+ status: number;
4
+ code: string;
5
+ message: string;
6
+ data: any;
7
+ constructor(response: Response, data?: any, code?: string);
8
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FetchError = void 0;
4
+ class FetchError extends Error {
5
+ constructor(response, data = {}, code = 'FETCH_ERROR') {
6
+ super();
7
+ this.name = 'FetchError';
8
+ const defaultMessage = 'An unspecified error occurred';
9
+ const getMessage = (data, locations) => locations
10
+ .map((item) => {
11
+ const parts = item.split('.');
12
+ let value = data;
13
+ for (const part of parts) {
14
+ value = value === null || value === void 0 ? void 0 : value[part];
15
+ }
16
+ return value;
17
+ })
18
+ .find((message) => typeof message === 'string') || null;
19
+ const messageFromData = getMessage(data, [
20
+ 'error.errors.0.message',
21
+ 'error.message',
22
+ 'message',
23
+ ]);
24
+ this.message = messageFromData || response.statusText || defaultMessage;
25
+ this.status = response.status;
26
+ this.code = code;
27
+ this.data = data;
28
+ this.message = this.message || defaultMessage;
29
+ Object.setPrototypeOf(this, new.target.prototype);
30
+ }
31
+ }
32
+ exports.FetchError = FetchError;
@@ -0,0 +1,28 @@
1
+ import { HookSettings, StatusCode } from './types';
2
+ /**
3
+ * Manages hooks for different status codes and their processing
4
+ */
5
+ export declare class HookManager {
6
+ private hooks;
7
+ private hookPromises;
8
+ /**
9
+ * Set hooks for different status codes
10
+ */
11
+ setHooks(hooks: Record<StatusCode, HookSettings>): void;
12
+ /**
13
+ * Get a hook for a specific status code
14
+ */
15
+ getHook(status: StatusCode): HookSettings | undefined;
16
+ /**
17
+ * Process a hook for a specific status code
18
+ */
19
+ processHook(accountId: string, status: StatusCode, error: any): Promise<Record<string, any> | null>;
20
+ /**
21
+ * Handle a retry failure with the appropriate hook
22
+ */
23
+ handleRetryFailure(accountId: string, status: StatusCode, error: any): Promise<void>;
24
+ /**
25
+ * Check if a hook exists and should retry for a given status
26
+ */
27
+ shouldRetry(status: StatusCode): boolean;
28
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HookManager = void 0;
4
+ /**
5
+ * Manages hooks for different status codes and their processing
6
+ */
7
+ class HookManager {
8
+ constructor() {
9
+ this.hooks = {};
10
+ this.hookPromises = {};
11
+ }
12
+ /**
13
+ * Set hooks for different status codes
14
+ */
15
+ setHooks(hooks) {
16
+ this.hooks = { ...this.hooks, ...hooks };
17
+ }
18
+ /**
19
+ * Get a hook for a specific status code
20
+ */
21
+ getHook(status) {
22
+ return this.hooks[status];
23
+ }
24
+ /**
25
+ * Process a hook for a specific status code
26
+ */
27
+ async processHook(accountId, status, error) {
28
+ const hook = this.hooks[status];
29
+ if (!hook || !hook.callback)
30
+ return null;
31
+ const hookKey = `${accountId}-${status}`;
32
+ try {
33
+ // Handle waiting for existing hook call if needed
34
+ if (hook.waitUntilFinished) {
35
+ if (!this.hookPromises[hookKey]) {
36
+ this.hookPromises[hookKey] = Promise.resolve(hook.callback(accountId, error.response) || {});
37
+ }
38
+ const result = await this.hookPromises[hookKey];
39
+ delete this.hookPromises[hookKey];
40
+ return result;
41
+ }
42
+ // Otherwise just call the hook directly
43
+ return await hook.callback(accountId, error.response) || {};
44
+ }
45
+ catch (hookError) {
46
+ console.error(`Hook callback failed for status ${status}:`, hookError);
47
+ if (hook.errorCallback) {
48
+ await hook.errorCallback(accountId, hookError);
49
+ }
50
+ throw hookError;
51
+ }
52
+ }
53
+ /**
54
+ * Handle a retry failure with the appropriate hook
55
+ */
56
+ async handleRetryFailure(accountId, status, error) {
57
+ const hook = this.hooks[status];
58
+ if (hook === null || hook === void 0 ? void 0 : hook.onRetryFail) {
59
+ await hook.onRetryFail(accountId, error);
60
+ }
61
+ }
62
+ /**
63
+ * Check if a hook exists and should retry for a given status
64
+ */
65
+ shouldRetry(status) {
66
+ const hook = this.hooks[status];
67
+ return !!hook && !!hook.retryCall;
68
+ }
69
+ }
70
+ exports.HookManager = HookManager;
@@ -0,0 +1,26 @@
1
+ import { ApiCallParams, Token } from './types';
2
+ /**
3
+ * Handles HTTP requests to external APIs
4
+ */
5
+ export declare class HttpClient {
6
+ /**
7
+ * Make an HTTP request
8
+ */
9
+ makeRequest(apiParams: ApiCallParams, authToken: Token | Record<string, any>): Promise<any>;
10
+ /**
11
+ * Build URL with query parameters
12
+ */
13
+ private buildUrl;
14
+ /**
15
+ * Prepare form data for file uploads
16
+ */
17
+ private prepareFormData;
18
+ /**
19
+ * Build fetch options for request
20
+ */
21
+ private buildFetchOptions;
22
+ /**
23
+ * Handle API response
24
+ */
25
+ private handleResponse;
26
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpClient = void 0;
7
+ const qs_1 = __importDefault(require("qs"));
8
+ const FetchError_1 = require("./FetchError");
9
+ const form_1 = require("./form");
10
+ /**
11
+ * Handles HTTP requests to external APIs
12
+ */
13
+ class HttpClient {
14
+ /**
15
+ * Make an HTTP request
16
+ */
17
+ async makeRequest(apiParams, authToken) {
18
+ const { accountId, method, route, base, body, data, headers, queryParams, contentType = 'application/json', accessToken: forcedAccessToken, requireAuth = true, files, } = apiParams;
19
+ // Build URL and request body
20
+ const url = this.buildUrl(base, route, queryParams);
21
+ const requestBody = body || data;
22
+ const normalizedMethod = method === null || method === void 0 ? void 0 : method.toUpperCase();
23
+ // Handle file uploads
24
+ const formData = this.prepareFormData(files);
25
+ // Create fetch options
26
+ const fetchOptions = this.buildFetchOptions({
27
+ method: normalizedMethod,
28
+ body: requestBody,
29
+ formData,
30
+ contentType,
31
+ authToken,
32
+ forcedAccessToken,
33
+ requireAuth,
34
+ headers,
35
+ });
36
+ // Make the request
37
+ try {
38
+ console.log(`🔄 Making API call to ${url}`);
39
+ const response = await fetch(url, fetchOptions);
40
+ return await this.handleResponse(response);
41
+ }
42
+ catch (error) {
43
+ console.error('🔄 Error making API call:', error);
44
+ throw error;
45
+ }
46
+ }
47
+ /**
48
+ * Build URL with query parameters
49
+ */
50
+ buildUrl(base, route, queryParams) {
51
+ let url = `${base}${route || ''}`;
52
+ if (queryParams)
53
+ url += `?${qs_1.default.stringify(queryParams)}`;
54
+ return url;
55
+ }
56
+ /**
57
+ * Prepare form data for file uploads
58
+ */
59
+ prepareFormData(files) {
60
+ if (!files)
61
+ return null;
62
+ const formData = (0, form_1.deserializeForm)(files);
63
+ // Use a workaround for TypeScript FormData entries() issue
64
+ const entries = formData;
65
+ for (let [key, value] of entries.entries()) {
66
+ console.log(`formdata ${key}:`, value);
67
+ }
68
+ return formData;
69
+ }
70
+ /**
71
+ * Build fetch options for request
72
+ */
73
+ buildFetchOptions({ method, body, formData, contentType, authToken, forcedAccessToken, requireAuth, headers, }) {
74
+ const allowedMethods = ['POST', 'PUT', 'PATCH'];
75
+ return {
76
+ method,
77
+ headers: {
78
+ ...(requireAuth && {
79
+ Authorization: `Bearer ${forcedAccessToken || authToken.access_token}`
80
+ }),
81
+ ...(!formData && { 'content-type': contentType }),
82
+ ...(headers || {}),
83
+ },
84
+ body: body && allowedMethods.includes(method)
85
+ ? JSON.stringify(body)
86
+ : (formData || null),
87
+ };
88
+ }
89
+ /**
90
+ * Handle API response
91
+ */
92
+ async handleResponse(response) {
93
+ let data = null;
94
+ try {
95
+ data = await response.json();
96
+ console.log('🔄 Response data:', data);
97
+ }
98
+ catch (error) {
99
+ // Response wasn't JSON, continue with null data
100
+ }
101
+ if (!response.ok) {
102
+ throw new FetchError_1.FetchError(response, data);
103
+ }
104
+ return data;
105
+ }
106
+ }
107
+ exports.HttpClient = HttpClient;
@@ -0,0 +1,36 @@
1
+ import { HookSettings } from './types';
2
+ /**
3
+ * Handles retry logic and delay strategies for failed API calls
4
+ */
5
+ export declare class RetryManager {
6
+ private defaultMaxDelay;
7
+ private defaultMaxRetries;
8
+ /**
9
+ * Default exponential backoff strategy with full jitter
10
+ */
11
+ private defaultDelayStrategy;
12
+ /**
13
+ * Calculate and wait for appropriate delay time before retry
14
+ */
15
+ calculateAndDelay(params: {
16
+ attempt: number;
17
+ response?: any;
18
+ hook: HookSettings;
19
+ }): Promise<void>;
20
+ /**
21
+ * Extract retry-after value from response
22
+ */
23
+ private getRetryAfterValue;
24
+ /**
25
+ * Get the default maximum number of retries
26
+ */
27
+ getDefaultMaxRetries(): number;
28
+ /**
29
+ * Set the default maximum number of retries
30
+ */
31
+ setDefaultMaxRetries(maxRetries: number): void;
32
+ /**
33
+ * Set the default maximum delay between retries
34
+ */
35
+ setDefaultMaxDelay(maxDelay: number): void;
36
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RetryManager = void 0;
4
+ /**
5
+ * Handles retry logic and delay strategies for failed API calls
6
+ */
7
+ class RetryManager {
8
+ constructor() {
9
+ this.defaultMaxDelay = 60000; // Default max delay of 1 minute
10
+ this.defaultMaxRetries = 4;
11
+ /**
12
+ * Default exponential backoff strategy with full jitter
13
+ */
14
+ this.defaultDelayStrategy = {
15
+ calculate: (attempt, response) => {
16
+ // Check for Retry-After header
17
+ const retryAfter = this.getRetryAfterValue(response);
18
+ if (retryAfter)
19
+ return retryAfter;
20
+ // Exponential backoff with full jitter
21
+ const baseDelay = 1000; // 1 second
22
+ const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
23
+ return Math.floor(Math.random() * exponentialDelay);
24
+ },
25
+ };
26
+ }
27
+ /**
28
+ * Calculate and wait for appropriate delay time before retry
29
+ */
30
+ async calculateAndDelay(params) {
31
+ const { attempt, response, hook } = params;
32
+ const delayStrategy = hook.delayStrategy || this.defaultDelayStrategy;
33
+ const maxDelay = hook.maxDelay || this.defaultMaxDelay;
34
+ const calculatedDelay = delayStrategy.calculate(attempt, response);
35
+ const finalDelay = Math.min(calculatedDelay, maxDelay);
36
+ console.log(`🔄 Waiting for ${finalDelay / 1000} seconds before retrying.`);
37
+ await new Promise(resolve => setTimeout(resolve, finalDelay));
38
+ }
39
+ /**
40
+ * Extract retry-after value from response
41
+ */
42
+ getRetryAfterValue(response) {
43
+ var _a;
44
+ if (!((_a = response === null || response === void 0 ? void 0 : response.headers) === null || _a === void 0 ? void 0 : _a.get))
45
+ return null;
46
+ const retryAfter = response.headers.get('Retry-After');
47
+ if (!retryAfter)
48
+ return null;
49
+ // Handle numeric retry-after
50
+ const parsedDelay = parseInt(retryAfter, 10);
51
+ if (!isNaN(parsedDelay)) {
52
+ return parsedDelay * 1000;
53
+ }
54
+ // Handle date retry-after
55
+ const date = new Date(retryAfter).getTime();
56
+ const now = Date.now();
57
+ if (date > now) {
58
+ return date - now;
59
+ }
60
+ return null;
61
+ }
62
+ /**
63
+ * Get the default maximum number of retries
64
+ */
65
+ getDefaultMaxRetries() {
66
+ return this.defaultMaxRetries;
67
+ }
68
+ /**
69
+ * Set the default maximum number of retries
70
+ */
71
+ setDefaultMaxRetries(maxRetries) {
72
+ this.defaultMaxRetries = maxRetries;
73
+ }
74
+ /**
75
+ * Set the default maximum delay between retries
76
+ */
77
+ setDefaultMaxDelay(maxDelay) {
78
+ this.defaultMaxDelay = maxDelay;
79
+ }
80
+ }
81
+ exports.RetryManager = RetryManager;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Barrel file exporting all ApiService components
3
+ */
4
+ export { CacheManager } from './CacheManager';
5
+ export { RetryManager } from './RetryManager';
6
+ export { HookManager } from './HookManager';
7
+ export { HttpClient } from './HttpClient';
8
+ export { AccountManager } from './AccountManager';
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ /**
3
+ * Barrel file exporting all ApiService components
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AccountManager = exports.HttpClient = exports.HookManager = exports.RetryManager = exports.CacheManager = void 0;
7
+ var CacheManager_1 = require("./CacheManager");
8
+ Object.defineProperty(exports, "CacheManager", { enumerable: true, get: function () { return CacheManager_1.CacheManager; } });
9
+ var RetryManager_1 = require("./RetryManager");
10
+ Object.defineProperty(exports, "RetryManager", { enumerable: true, get: function () { return RetryManager_1.RetryManager; } });
11
+ var HookManager_1 = require("./HookManager");
12
+ Object.defineProperty(exports, "HookManager", { enumerable: true, get: function () { return HookManager_1.HookManager; } });
13
+ var HttpClient_1 = require("./HttpClient");
14
+ Object.defineProperty(exports, "HttpClient", { enumerable: true, get: function () { return HttpClient_1.HttpClient; } });
15
+ var AccountManager_1 = require("./AccountManager");
16
+ Object.defineProperty(exports, "AccountManager", { enumerable: true, get: function () { return AccountManager_1.AccountManager; } });
package/dist/form.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function deserializeForm(src: any): FormData;
package/dist/form.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deserializeForm = void 0;
4
+ function deserializeForm(src) {
5
+ const fd = new FormData();
6
+ switch (src.cls) {
7
+ case 'FormData': {
8
+ for (const [key, items] of src.value) {
9
+ for (const item of items) {
10
+ let deserializedItem = deserializeForm(item);
11
+ if (deserializedItem instanceof FormData) {
12
+ // Use a workaround for TypeScript FormData entries() issue
13
+ const entries = deserializedItem;
14
+ for (const [subKey, subValue] of entries.entries()) {
15
+ fd.append(`${key}[${subKey}]`, subValue);
16
+ }
17
+ }
18
+ else {
19
+ fd.append(key, deserializedItem);
20
+ }
21
+ }
22
+ }
23
+ break;
24
+ }
25
+ case 'Blob':
26
+ case 'File': {
27
+ const { type, name, lastModified } = src;
28
+ const binStr = atob(src.value);
29
+ const arr = new Uint8Array(binStr.length);
30
+ for (let i = 0; i < binStr.length; i++)
31
+ arr[i] = binStr.charCodeAt(i);
32
+ const data = [arr.buffer];
33
+ const fileOrBlob = src.cls === 'File'
34
+ ? new File(data, name, { type, lastModified })
35
+ : new Blob(data, { type });
36
+ fd.append('file', fileOrBlob);
37
+ break;
38
+ }
39
+ case 'json': {
40
+ fd.append('json', JSON.stringify(JSON.parse(src.value)));
41
+ break;
42
+ }
43
+ default:
44
+ throw new Error('Unsupported type for deserialization');
45
+ }
46
+ return fd;
47
+ }
48
+ exports.deserializeForm = deserializeForm;
@@ -0,0 +1,50 @@
1
+ import { TokenService, ApiCallParams, HookSettings, StatusCode } from './types';
2
+ /**
3
+ * ApiService - Core API service for making authenticated API calls
4
+ * with caching, retry, and hook support.
5
+ */
6
+ declare class ApiService {
7
+ provider: string;
8
+ private tokenService;
9
+ private cacheManager;
10
+ private retryManager;
11
+ private hookManager;
12
+ private httpClient;
13
+ private accountManager;
14
+ private maxAttempts;
15
+ constructor();
16
+ /**
17
+ * Setup the API service
18
+ */
19
+ setup({ provider, tokenService, hooks, cacheTime, }: {
20
+ provider: string;
21
+ tokenService: TokenService;
22
+ hooks?: Record<StatusCode, HookSettings>;
23
+ cacheTime: number;
24
+ }): void;
25
+ /**
26
+ * Set the maximum number of retry attempts
27
+ */
28
+ setMaxAttempts(attempts: number): void;
29
+ /**
30
+ * Update account data
31
+ */
32
+ updateAccountData(accountId: string, data: Partial<Record<string, any>>): void;
33
+ /**
34
+ * Main API call method
35
+ */
36
+ makeApiCall(apiCallParams: ApiCallParams): Promise<any>;
37
+ /**
38
+ * Make a request with retry capability
39
+ */
40
+ private makeRequestWithRetry;
41
+ /**
42
+ * Set the cache time in milliseconds
43
+ */
44
+ setCacheTime(milliseconds: number): void;
45
+ /**
46
+ * Clear the cache
47
+ */
48
+ clearCache(): void;
49
+ }
50
+ export default ApiService;
package/dist/index.js ADDED
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const components_1 = require("./components");
4
+ /**
5
+ * ApiService - Core API service for making authenticated API calls
6
+ * with caching, retry, and hook support.
7
+ */
8
+ class ApiService {
9
+ constructor() {
10
+ // Default max attempts for API calls
11
+ this.maxAttempts = 10;
12
+ this.provider = '';
13
+ this.tokenService = {};
14
+ // Initialize component managers
15
+ this.cacheManager = new components_1.CacheManager();
16
+ this.retryManager = new components_1.RetryManager();
17
+ this.hookManager = new components_1.HookManager();
18
+ this.httpClient = new components_1.HttpClient();
19
+ this.accountManager = new components_1.AccountManager();
20
+ }
21
+ /**
22
+ * Setup the API service
23
+ */
24
+ setup({ provider, tokenService, hooks, cacheTime, }) {
25
+ this.provider = provider;
26
+ this.tokenService = tokenService;
27
+ if (hooks) {
28
+ this.hookManager.setHooks(hooks);
29
+ }
30
+ if (typeof cacheTime !== 'undefined') {
31
+ this.cacheManager.setCacheTime(cacheTime);
32
+ }
33
+ }
34
+ /**
35
+ * Set the maximum number of retry attempts
36
+ */
37
+ setMaxAttempts(attempts) {
38
+ this.maxAttempts = attempts;
39
+ }
40
+ /**
41
+ * Update account data
42
+ */
43
+ updateAccountData(accountId, data) {
44
+ this.accountManager.updateAccountData(accountId, data);
45
+ }
46
+ /**
47
+ * Main API call method
48
+ */
49
+ async makeApiCall(apiCallParams) {
50
+ console.log('🔄 makeApiCall', this.provider, apiCallParams.accountId);
51
+ // Check cache first
52
+ const cachedData = this.cacheManager.getFromCache(apiCallParams);
53
+ if (cachedData)
54
+ return cachedData;
55
+ // Make the API call with retry capability
56
+ const result = await this.makeRequestWithRetry(apiCallParams);
57
+ // Cache the result
58
+ this.cacheManager.saveToCache(apiCallParams, result);
59
+ return result;
60
+ }
61
+ /**
62
+ * Make a request with retry capability
63
+ */
64
+ async makeRequestWithRetry(apiCallParams) {
65
+ var _a;
66
+ const { accountId } = apiCallParams;
67
+ let attempts = 0;
68
+ const statusRetries = {};
69
+ // Copy the params to avoid mutation issues
70
+ let currentParams = { ...apiCallParams };
71
+ // Main retry loop
72
+ while (attempts < this.maxAttempts) {
73
+ attempts++;
74
+ try {
75
+ // Get authentication token if needed
76
+ const authToken = apiCallParams.requireAuth !== false
77
+ ? await this.tokenService.get(accountId)
78
+ : {};
79
+ // Verify we have authentication if required
80
+ if (apiCallParams.requireAuth !== false && !apiCallParams.accessToken && !authToken.access_token) {
81
+ throw new Error(`${this.provider} credentials not found for account ID ${accountId}`);
82
+ }
83
+ // Make the actual API call
84
+ const response = await this.httpClient.makeRequest(currentParams, authToken);
85
+ // Success - update account status and return result
86
+ this.accountManager.setLastRequestFailed(accountId, false);
87
+ return response;
88
+ }
89
+ catch (error) {
90
+ const status = error === null || error === void 0 ? void 0 : error.status;
91
+ // If no hook exists for this error, or we shouldn't retry, throw
92
+ if (!this.hookManager.shouldRetry(status)) {
93
+ throw error;
94
+ }
95
+ // Track retries for this status code
96
+ statusRetries[status] = (statusRetries[status] || 0) + 1;
97
+ const activeHook = this.hookManager.getHook(status);
98
+ const maxRetries = (_a = activeHook === null || activeHook === void 0 ? void 0 : activeHook.maxRetries) !== null && _a !== void 0 ? _a : this.retryManager.getDefaultMaxRetries();
99
+ // Check if we've exceeded retries for this status
100
+ if (statusRetries[status] > maxRetries) {
101
+ await this.hookManager.handleRetryFailure(accountId, status, error);
102
+ this.accountManager.setLastRequestFailed(accountId, true);
103
+ throw error;
104
+ }
105
+ // Process the hook to get updated params
106
+ try {
107
+ const hookResult = await this.hookManager.processHook(accountId, status, error);
108
+ if (hookResult) {
109
+ currentParams = { ...currentParams, ...hookResult };
110
+ }
111
+ }
112
+ catch (hookError) {
113
+ this.accountManager.setLastRequestFailed(accountId, true);
114
+ throw hookError;
115
+ }
116
+ // Wait before retrying if needed
117
+ if (activeHook === null || activeHook === void 0 ? void 0 : activeHook.retryDelay) {
118
+ await this.retryManager.calculateAndDelay({
119
+ attempt: statusRetries[status],
120
+ response: error.response,
121
+ hook: activeHook,
122
+ });
123
+ }
124
+ }
125
+ }
126
+ // If we've reached here, we've exceeded our maximum attempts
127
+ this.accountManager.setLastRequestFailed(accountId, true);
128
+ throw new Error(`Exceeded maximum attempts (${this.maxAttempts}) for API call to ${accountId}`);
129
+ }
130
+ /**
131
+ * Set the cache time in milliseconds
132
+ */
133
+ setCacheTime(milliseconds) {
134
+ this.cacheManager.setCacheTime(milliseconds);
135
+ }
136
+ /**
137
+ * Clear the cache
138
+ */
139
+ clearCache() {
140
+ this.cacheManager.clearCache();
141
+ }
142
+ }
143
+ exports.default = ApiService;
@@ -0,0 +1,59 @@
1
+ interface OAuthToken {
2
+ access_token: string;
3
+ expires_in: number;
4
+ id_token: string;
5
+ refresh_token: string;
6
+ scope: string;
7
+ token_type: string;
8
+ }
9
+ interface Token {
10
+ accountId: string;
11
+ access_token: string;
12
+ refresh_token: string;
13
+ provider: string;
14
+ enabled?: boolean;
15
+ updatedAt?: string;
16
+ primary?: boolean;
17
+ }
18
+ interface AccountData {
19
+ lastRequestTime?: number;
20
+ lastFailed?: boolean;
21
+ token?: Token;
22
+ }
23
+ interface DelayStrategy {
24
+ calculate: (attempt: number, response?: any) => number;
25
+ }
26
+ interface ApiCallParams {
27
+ accountId: string;
28
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
29
+ route: string;
30
+ base: string;
31
+ body?: object;
32
+ data?: object;
33
+ headers?: Record<string, string>;
34
+ queryParams?: URLSearchParams;
35
+ accessToken?: string;
36
+ requireAuth?: boolean;
37
+ noContentType?: boolean;
38
+ contentType?: string;
39
+ cacheTime?: number;
40
+ files?: File[];
41
+ }
42
+ interface HookSettings {
43
+ retryCall: boolean;
44
+ retryDelay: boolean;
45
+ waitUntilFinished?: boolean;
46
+ maxRetries?: number;
47
+ callback: (accountId: string, response: any) => Promise<any>;
48
+ onRetryFail?: (accountId: string, error: any) => Promise<void>;
49
+ errorCallback?: (accountId: string, error: any) => Promise<void>;
50
+ delayStrategy?: DelayStrategy;
51
+ maxDelay?: number;
52
+ }
53
+ type StatusCode = string | number;
54
+ type TokenService = {
55
+ get: (accountId: string) => Promise<Token>;
56
+ set: (accountId: string, token: Partial<Token>) => Promise<void>;
57
+ refresh?: (accountId: string, refreshToken: string) => Promise<OAuthToken>;
58
+ };
59
+ export { OAuthToken, DelayStrategy, Token, AccountData, ApiCallParams, HookSettings, StatusCode, TokenService, };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@rendomnet/apiservice",
3
+ "version": "1.0.0",
4
+ "description": "A robust TypeScript API service framework for making authenticated API calls",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "clean": "rimraf dist",
13
+ "prebuild": "npm run clean",
14
+ "prepublishOnly": "npm run build",
15
+ "test": "echo \"Error: no test specified\" && exit 1",
16
+ "prepare": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "api",
20
+ "service",
21
+ "typescript",
22
+ "fetch",
23
+ "retry",
24
+ "cache",
25
+ "hooks"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/rendomnet/apiservice.git"
30
+ },
31
+ "author": "",
32
+ "license": "MIT",
33
+ "bugs": {
34
+ "url": "https://github.com/rendomnet/apiservice/issues"
35
+ },
36
+ "homepage": "https://github.com/rendomnet/apiservice#readme",
37
+ "dependencies": {
38
+ "qs": "^6.11.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^18.0.0",
42
+ "@types/qs": "^6.9.7",
43
+ "rimraf": "^3.0.2",
44
+ "typescript": "^4.7.4"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }