api-core-lib 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/README.md ADDED
File without changes
@@ -0,0 +1,145 @@
1
+ import { AxiosRequestConfig, AxiosInstance } from 'axios';
2
+ import * as react from 'react';
3
+
4
+ interface PaginationMeta {
5
+ itemsPerPage: number;
6
+ totalItems: number;
7
+ currentPage: number;
8
+ totalPages: number;
9
+ sortBy?: [string, 'asc' | 'desc'][];
10
+ }
11
+ interface ApiLinks {
12
+ current: string;
13
+ first?: string;
14
+ last?: string;
15
+ next?: string;
16
+ prev?: string;
17
+ }
18
+ interface ApiResponse<T = any> {
19
+ data: T;
20
+ meta?: PaginationMeta;
21
+ links?: ApiLinks;
22
+ message?: string;
23
+ timestamp?: string;
24
+ }
25
+ interface ValidationError {
26
+ field: string;
27
+ message: string;
28
+ code: string;
29
+ }
30
+ interface ApiError {
31
+ message: string;
32
+ status: number;
33
+ code?: string;
34
+ errors?: ValidationError[];
35
+ timestamp?: string;
36
+ path?: string;
37
+ requestId?: string;
38
+ }
39
+ interface StandardResponse<T> {
40
+ response?: ApiResponse<T> | T;
41
+ error: ApiError | null;
42
+ loading: boolean;
43
+ success: boolean;
44
+ message?: string;
45
+ cached?: boolean;
46
+ validationErrors?: ValidationError[];
47
+ }
48
+ interface CacheConfig {
49
+ enabled: boolean;
50
+ duration?: number;
51
+ }
52
+ interface RequestConfig extends AxiosRequestConfig {
53
+ retry?: boolean;
54
+ retryCount?: number;
55
+ cache?: boolean | CacheConfig;
56
+ cancelTokenKey?: string;
57
+ }
58
+ interface ApiClientConfig {
59
+ baseURL: string;
60
+ timeout?: number;
61
+ headers?: Record<string, string>;
62
+ getAccessToken?: () => string | null | Promise<string | null>;
63
+ getRefreshToken?: () => string | null | Promise<string | null>;
64
+ handleTokenRefresh?: (refreshToken: string) => Promise<{
65
+ newAccessToken: string;
66
+ newRefreshToken: string;
67
+ }>;
68
+ onTokenRefreshSuccess?: (newAccessToken: string, newRefreshToken: string) => void;
69
+ onTokenRefreshError?: (error: any) => void;
70
+ }
71
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
72
+ interface PaginateQueryOptions {
73
+ page?: number;
74
+ limit?: number;
75
+ search?: string;
76
+ sortBy?: {
77
+ key: string;
78
+ direction: 'asc' | 'desc';
79
+ }[];
80
+ filter?: Record<string, any>;
81
+ [key: string]: any;
82
+ }
83
+ interface UseApiConfig<T> {
84
+ endpoint: string;
85
+ reduxKey?: string;
86
+ initialData?: T | T[];
87
+ initialQuery?: PaginateQueryOptions;
88
+ enabled?: boolean;
89
+ refetchAfterChange?: boolean;
90
+ isToastAfterActions?: boolean;
91
+ onSuccess?: (message: string, data?: T) => void;
92
+ onError?: (message: string, error?: ApiError) => void;
93
+ }
94
+
95
+ declare function createApiClient(config: ApiClientConfig): AxiosInstance;
96
+
97
+ declare class CacheManager {
98
+ private cache;
99
+ private defaultDuration;
100
+ set<T>(key: string, data: T, duration?: number): void;
101
+ get<T>(key: string): T | null;
102
+ delete(key: string): void;
103
+ clear(): void;
104
+ }
105
+ declare const cacheManager: CacheManager;
106
+
107
+ declare function createApiServices<T>(axiosInstance: AxiosInstance, endpoint: string): {
108
+ get: (id?: string, config?: RequestConfig) => Promise<StandardResponse<T | T[]>>;
109
+ getWithQuery: (query: string, config?: RequestConfig) => Promise<StandardResponse<T[]>>;
110
+ post: (data: Partial<T>, config?: RequestConfig) => Promise<StandardResponse<T>>;
111
+ patch: (id: string, data: Partial<T>, config?: RequestConfig) => Promise<StandardResponse<T>>;
112
+ remove: (id: string, config?: RequestConfig) => Promise<StandardResponse<any>>;
113
+ };
114
+
115
+ declare const cancelRequest: (key: string) => void;
116
+ declare const cancelAllRequests: () => void;
117
+ declare function buildPaginateQuery(query: PaginateQueryOptions): string;
118
+
119
+ declare function useApi<T extends {
120
+ id?: string | number;
121
+ }>(axiosInstance: AxiosInstance, config: UseApiConfig<T>): {
122
+ state: StandardResponse<T | T[]>;
123
+ setState: react.Dispatch<react.SetStateAction<StandardResponse<T | T[]>>>;
124
+ actions: {
125
+ fetch: () => Promise<void>;
126
+ create: (newItem: Partial<T>) => Promise<StandardResponse<T>>;
127
+ update: (id: string, updatedItem: Partial<T>) => Promise<StandardResponse<T>>;
128
+ remove: (id: string) => Promise<StandardResponse<any>>;
129
+ };
130
+ query: {
131
+ options: PaginateQueryOptions;
132
+ setOptions: react.Dispatch<react.SetStateAction<PaginateQueryOptions>>;
133
+ setPage: (page: number) => void;
134
+ setLimit: (limit: number) => void;
135
+ setSearchTerm: (search: string) => void;
136
+ setSorting: (sortBy: {
137
+ key: string;
138
+ direction: "asc" | "desc";
139
+ }[]) => void;
140
+ setFilters: (filter: Record<string, any>) => void;
141
+ reset: () => void;
142
+ };
143
+ };
144
+
145
+ export { type ApiClientConfig, type ApiError, type ApiLinks, type ApiResponse, type CacheConfig, type HttpMethod, type PaginateQueryOptions, type PaginationMeta, type RequestConfig, type StandardResponse, type UseApiConfig, type ValidationError, buildPaginateQuery, cacheManager, cancelAllRequests, cancelRequest, createApiClient, createApiServices, useApi };
@@ -0,0 +1,145 @@
1
+ import { AxiosRequestConfig, AxiosInstance } from 'axios';
2
+ import * as react from 'react';
3
+
4
+ interface PaginationMeta {
5
+ itemsPerPage: number;
6
+ totalItems: number;
7
+ currentPage: number;
8
+ totalPages: number;
9
+ sortBy?: [string, 'asc' | 'desc'][];
10
+ }
11
+ interface ApiLinks {
12
+ current: string;
13
+ first?: string;
14
+ last?: string;
15
+ next?: string;
16
+ prev?: string;
17
+ }
18
+ interface ApiResponse<T = any> {
19
+ data: T;
20
+ meta?: PaginationMeta;
21
+ links?: ApiLinks;
22
+ message?: string;
23
+ timestamp?: string;
24
+ }
25
+ interface ValidationError {
26
+ field: string;
27
+ message: string;
28
+ code: string;
29
+ }
30
+ interface ApiError {
31
+ message: string;
32
+ status: number;
33
+ code?: string;
34
+ errors?: ValidationError[];
35
+ timestamp?: string;
36
+ path?: string;
37
+ requestId?: string;
38
+ }
39
+ interface StandardResponse<T> {
40
+ response?: ApiResponse<T> | T;
41
+ error: ApiError | null;
42
+ loading: boolean;
43
+ success: boolean;
44
+ message?: string;
45
+ cached?: boolean;
46
+ validationErrors?: ValidationError[];
47
+ }
48
+ interface CacheConfig {
49
+ enabled: boolean;
50
+ duration?: number;
51
+ }
52
+ interface RequestConfig extends AxiosRequestConfig {
53
+ retry?: boolean;
54
+ retryCount?: number;
55
+ cache?: boolean | CacheConfig;
56
+ cancelTokenKey?: string;
57
+ }
58
+ interface ApiClientConfig {
59
+ baseURL: string;
60
+ timeout?: number;
61
+ headers?: Record<string, string>;
62
+ getAccessToken?: () => string | null | Promise<string | null>;
63
+ getRefreshToken?: () => string | null | Promise<string | null>;
64
+ handleTokenRefresh?: (refreshToken: string) => Promise<{
65
+ newAccessToken: string;
66
+ newRefreshToken: string;
67
+ }>;
68
+ onTokenRefreshSuccess?: (newAccessToken: string, newRefreshToken: string) => void;
69
+ onTokenRefreshError?: (error: any) => void;
70
+ }
71
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
72
+ interface PaginateQueryOptions {
73
+ page?: number;
74
+ limit?: number;
75
+ search?: string;
76
+ sortBy?: {
77
+ key: string;
78
+ direction: 'asc' | 'desc';
79
+ }[];
80
+ filter?: Record<string, any>;
81
+ [key: string]: any;
82
+ }
83
+ interface UseApiConfig<T> {
84
+ endpoint: string;
85
+ reduxKey?: string;
86
+ initialData?: T | T[];
87
+ initialQuery?: PaginateQueryOptions;
88
+ enabled?: boolean;
89
+ refetchAfterChange?: boolean;
90
+ isToastAfterActions?: boolean;
91
+ onSuccess?: (message: string, data?: T) => void;
92
+ onError?: (message: string, error?: ApiError) => void;
93
+ }
94
+
95
+ declare function createApiClient(config: ApiClientConfig): AxiosInstance;
96
+
97
+ declare class CacheManager {
98
+ private cache;
99
+ private defaultDuration;
100
+ set<T>(key: string, data: T, duration?: number): void;
101
+ get<T>(key: string): T | null;
102
+ delete(key: string): void;
103
+ clear(): void;
104
+ }
105
+ declare const cacheManager: CacheManager;
106
+
107
+ declare function createApiServices<T>(axiosInstance: AxiosInstance, endpoint: string): {
108
+ get: (id?: string, config?: RequestConfig) => Promise<StandardResponse<T | T[]>>;
109
+ getWithQuery: (query: string, config?: RequestConfig) => Promise<StandardResponse<T[]>>;
110
+ post: (data: Partial<T>, config?: RequestConfig) => Promise<StandardResponse<T>>;
111
+ patch: (id: string, data: Partial<T>, config?: RequestConfig) => Promise<StandardResponse<T>>;
112
+ remove: (id: string, config?: RequestConfig) => Promise<StandardResponse<any>>;
113
+ };
114
+
115
+ declare const cancelRequest: (key: string) => void;
116
+ declare const cancelAllRequests: () => void;
117
+ declare function buildPaginateQuery(query: PaginateQueryOptions): string;
118
+
119
+ declare function useApi<T extends {
120
+ id?: string | number;
121
+ }>(axiosInstance: AxiosInstance, config: UseApiConfig<T>): {
122
+ state: StandardResponse<T | T[]>;
123
+ setState: react.Dispatch<react.SetStateAction<StandardResponse<T | T[]>>>;
124
+ actions: {
125
+ fetch: () => Promise<void>;
126
+ create: (newItem: Partial<T>) => Promise<StandardResponse<T>>;
127
+ update: (id: string, updatedItem: Partial<T>) => Promise<StandardResponse<T>>;
128
+ remove: (id: string) => Promise<StandardResponse<any>>;
129
+ };
130
+ query: {
131
+ options: PaginateQueryOptions;
132
+ setOptions: react.Dispatch<react.SetStateAction<PaginateQueryOptions>>;
133
+ setPage: (page: number) => void;
134
+ setLimit: (limit: number) => void;
135
+ setSearchTerm: (search: string) => void;
136
+ setSorting: (sortBy: {
137
+ key: string;
138
+ direction: "asc" | "desc";
139
+ }[]) => void;
140
+ setFilters: (filter: Record<string, any>) => void;
141
+ reset: () => void;
142
+ };
143
+ };
144
+
145
+ export { type ApiClientConfig, type ApiError, type ApiLinks, type ApiResponse, type CacheConfig, type HttpMethod, type PaginateQueryOptions, type PaginationMeta, type RequestConfig, type StandardResponse, type UseApiConfig, type ValidationError, buildPaginateQuery, cacheManager, cancelAllRequests, cancelRequest, createApiClient, createApiServices, useApi };
package/dist/index.js ADDED
@@ -0,0 +1,440 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ buildPaginateQuery: () => buildPaginateQuery,
34
+ cacheManager: () => cacheManager,
35
+ cancelAllRequests: () => cancelAllRequests,
36
+ cancelRequest: () => cancelRequest,
37
+ createApiClient: () => createApiClient,
38
+ createApiServices: () => createApiServices,
39
+ useApi: () => useApi
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/core/client.ts
44
+ var import_axios2 = __toESM(require("axios"));
45
+ var import_axios_retry = __toESM(require("axios-retry"));
46
+ var import_uuid = require("uuid");
47
+
48
+ // src/core/cache.ts
49
+ var CacheManager = class {
50
+ cache = /* @__PURE__ */ new Map();
51
+ defaultDuration = 15 * 60 * 1e3;
52
+ // 15 minutes
53
+ set(key, data, duration) {
54
+ this.cache.set(key, {
55
+ data,
56
+ timestamp: Date.now(),
57
+ duration: duration || this.defaultDuration
58
+ });
59
+ }
60
+ get(key) {
61
+ const item = this.cache.get(key);
62
+ if (!item) return null;
63
+ const isExpired = Date.now() - item.timestamp > item.duration;
64
+ if (isExpired) {
65
+ this.cache.delete(key);
66
+ return null;
67
+ }
68
+ return item.data;
69
+ }
70
+ delete(key) {
71
+ this.cache.delete(key);
72
+ }
73
+ clear() {
74
+ this.cache.clear();
75
+ }
76
+ };
77
+ var cacheManager = new CacheManager();
78
+
79
+ // src/core/utils.ts
80
+ var import_axios = __toESM(require("axios"));
81
+ var cancelTokens = /* @__PURE__ */ new Map();
82
+ var createCancelToken = (key) => {
83
+ const existingToken = cancelTokens.get(key);
84
+ if (existingToken) {
85
+ existingToken.cancel("Operation canceled due to new request.");
86
+ cancelTokens.delete(key);
87
+ }
88
+ const source = import_axios.default.CancelToken.source();
89
+ cancelTokens.set(key, source);
90
+ return source;
91
+ };
92
+ var cancelRequest = (key) => {
93
+ const source = cancelTokens.get(key);
94
+ if (source) {
95
+ source.cancel("Operation canceled by user.");
96
+ cancelTokens.delete(key);
97
+ }
98
+ };
99
+ var cancelAllRequests = () => {
100
+ cancelTokens.forEach((source) => source.cancel("All operations canceled."));
101
+ cancelTokens.clear();
102
+ };
103
+ function buildPaginateQuery(query) {
104
+ const params = new URLSearchParams();
105
+ if (!query) return "";
106
+ if (query.page) params.append("page", query.page.toString());
107
+ if (query.limit) params.append("limit", query.limit.toString());
108
+ if (query.search) params.append("search", query.search);
109
+ if (query.sortBy) {
110
+ query.sortBy.forEach((sort) => params.append("sortBy", `${sort.key}:${sort.direction}`));
111
+ }
112
+ if (query.filter) {
113
+ Object.entries(query.filter).forEach(([field, value]) => {
114
+ params.append(`filter.${field}`, value);
115
+ });
116
+ }
117
+ const queryString = params.toString();
118
+ return queryString ? `?${queryString}` : "";
119
+ }
120
+ function isApiError(obj) {
121
+ return obj && typeof obj.status === "number" && typeof obj.message === "string" && obj.config === void 0;
122
+ }
123
+ function isAxiosResponse(obj) {
124
+ return obj && obj.data !== void 0 && obj.status !== void 0 && obj.config !== void 0;
125
+ }
126
+
127
+ // src/core/client.ts
128
+ function createApiClient(config) {
129
+ const {
130
+ baseURL,
131
+ timeout = 1e4,
132
+ headers = {},
133
+ getAccessToken,
134
+ getRefreshToken,
135
+ handleTokenRefresh,
136
+ onTokenRefreshSuccess,
137
+ onTokenRefreshError
138
+ } = config;
139
+ const axiosInstance = import_axios2.default.create({
140
+ baseURL,
141
+ timeout,
142
+ headers: {
143
+ "Content-Type": "application/json",
144
+ Accept: "application/json",
145
+ ...headers
146
+ },
147
+ withCredentials: true
148
+ });
149
+ (0, import_axios_retry.default)(axiosInstance, {
150
+ retries: 3,
151
+ retryDelay: import_axios_retry.default.exponentialDelay,
152
+ retryCondition: (error) => import_axios_retry.default.isNetworkOrIdempotentRequestError(error) || error.response?.status === 429
153
+ });
154
+ let isRefreshing = false;
155
+ let requestQueue = [];
156
+ axiosInstance.interceptors.request.use(async (req) => {
157
+ req.headers["X-Request-ID"] = (0, import_uuid.v4)();
158
+ if (getAccessToken) {
159
+ const token = await getAccessToken();
160
+ if (token) {
161
+ req.headers.Authorization = `Bearer ${token}`;
162
+ }
163
+ }
164
+ if (req.cancelTokenKey) {
165
+ req.cancelToken = createCancelToken(req.cancelTokenKey).token;
166
+ }
167
+ if (req.method?.toLowerCase() === "get" && req.cache) {
168
+ const cacheKey = req.url || "default-cache-key";
169
+ const cachedData = cacheManager.get(cacheKey);
170
+ if (cachedData) {
171
+ console.log(`[Cache] HIT for ${cacheKey}`);
172
+ req.adapter = () => Promise.resolve({
173
+ data: cachedData,
174
+ status: 200,
175
+ statusText: "OK (from cache)",
176
+ headers: req.headers,
177
+ config: req
178
+ });
179
+ req.cached = true;
180
+ }
181
+ }
182
+ return req;
183
+ }, (error) => Promise.reject(error));
184
+ axiosInstance.interceptors.response.use(
185
+ (response) => {
186
+ const config2 = response.config;
187
+ if (response.config.method?.toLowerCase() === "get" && config2.cache) {
188
+ const cacheKey = response.config.url || "default-cache-key";
189
+ const cacheConfig = typeof config2.cache === "object" ? config2.cache : { enabled: true };
190
+ if (cacheConfig.enabled) {
191
+ console.log(`[Cache] SET for ${cacheKey}`);
192
+ cacheManager.set(cacheKey, response.data, cacheConfig.duration);
193
+ }
194
+ }
195
+ return response;
196
+ },
197
+ async (error) => {
198
+ const originalRequest = error.config;
199
+ if (error.response?.status === 401 && !originalRequest._retry && getRefreshToken && handleTokenRefresh) {
200
+ originalRequest._retry = true;
201
+ if (isRefreshing) {
202
+ return new Promise((resolve) => {
203
+ requestQueue.push((token) => {
204
+ originalRequest.headers.Authorization = `Bearer ${token}`;
205
+ resolve(axiosInstance(originalRequest));
206
+ });
207
+ });
208
+ }
209
+ isRefreshing = true;
210
+ try {
211
+ const currentRefreshToken = await getRefreshToken();
212
+ if (!currentRefreshToken) throw new Error("No refresh token available.");
213
+ const { newAccessToken, newRefreshToken } = await handleTokenRefresh(currentRefreshToken);
214
+ if (onTokenRefreshSuccess) {
215
+ onTokenRefreshSuccess(newAccessToken, newRefreshToken);
216
+ }
217
+ axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`;
218
+ originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
219
+ requestQueue.forEach((cb) => cb(newAccessToken));
220
+ requestQueue = [];
221
+ return axiosInstance(originalRequest);
222
+ } catch (refreshError) {
223
+ if (onTokenRefreshError) {
224
+ onTokenRefreshError(refreshError);
225
+ }
226
+ requestQueue = [];
227
+ return Promise.reject(refreshError);
228
+ } finally {
229
+ isRefreshing = false;
230
+ }
231
+ }
232
+ const enhancedError = {
233
+ message: error.response?.data?.message || error.message || "An unexpected error occurred",
234
+ status: error.response?.status || 500,
235
+ code: error.code,
236
+ errors: error.response?.data?.errors,
237
+ path: originalRequest.url,
238
+ requestId: originalRequest.headers?.["X-Request-ID"]
239
+ };
240
+ return Promise.reject(enhancedError);
241
+ }
242
+ );
243
+ return axiosInstance;
244
+ }
245
+
246
+ // src/core/processor.ts
247
+ var processResponse = (responseOrError) => {
248
+ if (isApiError(responseOrError)) {
249
+ return {
250
+ response: void 0,
251
+ error: responseOrError,
252
+ validationErrors: responseOrError.errors || [],
253
+ success: false,
254
+ loading: false,
255
+ message: responseOrError.message
256
+ };
257
+ }
258
+ if (isAxiosResponse(responseOrError)) {
259
+ const response = responseOrError;
260
+ const isSuccess = response.status >= 200 && response.status < 300;
261
+ if (isSuccess) {
262
+ return {
263
+ response: response.data,
264
+ loading: false,
265
+ success: true,
266
+ cached: response.config.cached,
267
+ error: null,
268
+ validationErrors: [],
269
+ message: response.data.message || "Request successful."
270
+ };
271
+ }
272
+ }
273
+ return {
274
+ response: void 0,
275
+ error: {
276
+ message: "An unknown error occurred during response processing.",
277
+ status: 500
278
+ },
279
+ success: false,
280
+ loading: false
281
+ };
282
+ };
283
+
284
+ // src/services/crud.ts
285
+ function createApiServices(axiosInstance, endpoint) {
286
+ const get = async (id, config) => {
287
+ const url = id ? `${endpoint}/${id}` : endpoint;
288
+ try {
289
+ const response = await axiosInstance.get(url, config);
290
+ return processResponse(response);
291
+ } catch (error) {
292
+ return processResponse(error);
293
+ }
294
+ };
295
+ const getWithQuery = async (query, config) => {
296
+ try {
297
+ const response = await axiosInstance.get(`${endpoint}${query}`, config);
298
+ return processResponse(response);
299
+ } catch (error) {
300
+ return processResponse(error);
301
+ }
302
+ };
303
+ const post = async (data, config) => {
304
+ try {
305
+ const response = await axiosInstance.post(endpoint, data, config);
306
+ return processResponse(response);
307
+ } catch (error) {
308
+ return processResponse(error);
309
+ }
310
+ };
311
+ const patch = async (id, data, config) => {
312
+ try {
313
+ const response = await axiosInstance.patch(`${endpoint}/${id}`, data, config);
314
+ return processResponse(response);
315
+ } catch (error) {
316
+ return processResponse(error);
317
+ }
318
+ };
319
+ const remove = async (id, config) => {
320
+ try {
321
+ const response = await axiosInstance.delete(`${endpoint}/${id}`, config);
322
+ return processResponse(response);
323
+ } catch (error) {
324
+ return processResponse(error);
325
+ }
326
+ };
327
+ return { get, getWithQuery, post, patch, remove };
328
+ }
329
+
330
+ // src/hooks/useApi.ts
331
+ var import_react = require("react");
332
+ function useApi(axiosInstance, config) {
333
+ const {
334
+ endpoint,
335
+ initialData,
336
+ initialQuery = { page: 1, limit: 10 },
337
+ enabled = true,
338
+ refetchAfterChange = true,
339
+ onSuccess,
340
+ onError
341
+ } = config;
342
+ const [state, setState] = (0, import_react.useState)({
343
+ response: initialData || void 0,
344
+ loading: enabled,
345
+ error: null,
346
+ success: false
347
+ });
348
+ const [queryOptions, setQueryOptions] = (0, import_react.useState)(initialQuery);
349
+ const apiServices = (0, import_react.useRef)(createApiServices(axiosInstance, endpoint)).current;
350
+ const fetchData = (0, import_react.useCallback)(async () => {
351
+ setState((prev) => ({ ...prev, loading: true, error: null }));
352
+ const queryString = buildPaginateQuery(queryOptions);
353
+ const result = await apiServices.getWithQuery(queryString, { cancelTokenKey: endpoint });
354
+ setState(result);
355
+ if (!result.success && onError) {
356
+ onError(result.message || "Fetch failed", result.error || void 0);
357
+ }
358
+ }, [apiServices, queryOptions, endpoint, onError]);
359
+ (0, import_react.useEffect)(() => {
360
+ if (enabled) {
361
+ fetchData();
362
+ }
363
+ }, [enabled, queryOptions]);
364
+ const createItem = async (newItem) => {
365
+ setState((prev) => ({ ...prev, loading: true }));
366
+ const result = await apiServices.post(newItem);
367
+ if (result.success) {
368
+ if (refetchAfterChange) await fetchData();
369
+ else setState((prev) => ({ ...prev, loading: false }));
370
+ if (onSuccess) onSuccess(result.message || "Item created successfully!", result.response);
371
+ } else {
372
+ setState((prev) => ({ ...prev, loading: false, error: result.error }));
373
+ if (onError) onError(result.message || "Create failed", result.error || void 0);
374
+ }
375
+ return result;
376
+ };
377
+ const updateItem = async (id, updatedItem) => {
378
+ setState((prev) => ({ ...prev, loading: true }));
379
+ const result = await apiServices.patch(id, updatedItem);
380
+ if (result.success) {
381
+ if (refetchAfterChange) await fetchData();
382
+ else setState((prev) => ({ ...prev, loading: false }));
383
+ if (onSuccess) onSuccess(result.message || "Item updated successfully!", result.response);
384
+ } else {
385
+ setState((prev) => ({ ...prev, loading: false, error: result.error }));
386
+ if (onError) onError(result.message || "Update failed", result.error || void 0);
387
+ }
388
+ return result;
389
+ };
390
+ const deleteItem = async (id) => {
391
+ setState((prev) => ({ ...prev, loading: true }));
392
+ const result = await apiServices.remove(id);
393
+ if (result.success) {
394
+ if (refetchAfterChange) await fetchData();
395
+ else setState((prev) => ({ ...prev, loading: false }));
396
+ if (onSuccess) onSuccess(result.message || "Item deleted successfully!");
397
+ } else {
398
+ setState((prev) => ({ ...prev, loading: false, error: result.error }));
399
+ if (onError) onError(result.message || "Delete failed", result.error || void 0);
400
+ }
401
+ return result;
402
+ };
403
+ const setPage = (page) => setQueryOptions((prev) => ({ ...prev, page }));
404
+ const setLimit = (limit) => setQueryOptions((prev) => ({ ...prev, limit, page: 1 }));
405
+ const setSearchTerm = (search) => setQueryOptions((prev) => ({ ...prev, search, page: 1 }));
406
+ const setSorting = (sortBy) => setQueryOptions((prev) => ({ ...prev, sortBy }));
407
+ const setFilters = (filter) => setQueryOptions((prev) => ({ ...prev, filter, page: 1 }));
408
+ const resetQuery = () => setQueryOptions(initialQuery);
409
+ return {
410
+ state,
411
+ setState,
412
+ // Exposing setState for direct manipulation or connection to Redux
413
+ actions: {
414
+ fetch: fetchData,
415
+ create: createItem,
416
+ update: updateItem,
417
+ remove: deleteItem
418
+ },
419
+ query: {
420
+ options: queryOptions,
421
+ setOptions: setQueryOptions,
422
+ setPage,
423
+ setLimit,
424
+ setSearchTerm,
425
+ setSorting,
426
+ setFilters,
427
+ reset: resetQuery
428
+ }
429
+ };
430
+ }
431
+ // Annotate the CommonJS export names for ESM import in node:
432
+ 0 && (module.exports = {
433
+ buildPaginateQuery,
434
+ cacheManager,
435
+ cancelAllRequests,
436
+ cancelRequest,
437
+ createApiClient,
438
+ createApiServices,
439
+ useApi
440
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,397 @@
1
+ // src/core/client.ts
2
+ import axios2 from "axios";
3
+ import axiosRetry from "axios-retry";
4
+ import { v4 as uuidv4 } from "uuid";
5
+
6
+ // src/core/cache.ts
7
+ var CacheManager = class {
8
+ cache = /* @__PURE__ */ new Map();
9
+ defaultDuration = 15 * 60 * 1e3;
10
+ // 15 minutes
11
+ set(key, data, duration) {
12
+ this.cache.set(key, {
13
+ data,
14
+ timestamp: Date.now(),
15
+ duration: duration || this.defaultDuration
16
+ });
17
+ }
18
+ get(key) {
19
+ const item = this.cache.get(key);
20
+ if (!item) return null;
21
+ const isExpired = Date.now() - item.timestamp > item.duration;
22
+ if (isExpired) {
23
+ this.cache.delete(key);
24
+ return null;
25
+ }
26
+ return item.data;
27
+ }
28
+ delete(key) {
29
+ this.cache.delete(key);
30
+ }
31
+ clear() {
32
+ this.cache.clear();
33
+ }
34
+ };
35
+ var cacheManager = new CacheManager();
36
+
37
+ // src/core/utils.ts
38
+ import axios from "axios";
39
+ var cancelTokens = /* @__PURE__ */ new Map();
40
+ var createCancelToken = (key) => {
41
+ const existingToken = cancelTokens.get(key);
42
+ if (existingToken) {
43
+ existingToken.cancel("Operation canceled due to new request.");
44
+ cancelTokens.delete(key);
45
+ }
46
+ const source = axios.CancelToken.source();
47
+ cancelTokens.set(key, source);
48
+ return source;
49
+ };
50
+ var cancelRequest = (key) => {
51
+ const source = cancelTokens.get(key);
52
+ if (source) {
53
+ source.cancel("Operation canceled by user.");
54
+ cancelTokens.delete(key);
55
+ }
56
+ };
57
+ var cancelAllRequests = () => {
58
+ cancelTokens.forEach((source) => source.cancel("All operations canceled."));
59
+ cancelTokens.clear();
60
+ };
61
+ function buildPaginateQuery(query) {
62
+ const params = new URLSearchParams();
63
+ if (!query) return "";
64
+ if (query.page) params.append("page", query.page.toString());
65
+ if (query.limit) params.append("limit", query.limit.toString());
66
+ if (query.search) params.append("search", query.search);
67
+ if (query.sortBy) {
68
+ query.sortBy.forEach((sort) => params.append("sortBy", `${sort.key}:${sort.direction}`));
69
+ }
70
+ if (query.filter) {
71
+ Object.entries(query.filter).forEach(([field, value]) => {
72
+ params.append(`filter.${field}`, value);
73
+ });
74
+ }
75
+ const queryString = params.toString();
76
+ return queryString ? `?${queryString}` : "";
77
+ }
78
+ function isApiError(obj) {
79
+ return obj && typeof obj.status === "number" && typeof obj.message === "string" && obj.config === void 0;
80
+ }
81
+ function isAxiosResponse(obj) {
82
+ return obj && obj.data !== void 0 && obj.status !== void 0 && obj.config !== void 0;
83
+ }
84
+
85
+ // src/core/client.ts
86
+ function createApiClient(config) {
87
+ const {
88
+ baseURL,
89
+ timeout = 1e4,
90
+ headers = {},
91
+ getAccessToken,
92
+ getRefreshToken,
93
+ handleTokenRefresh,
94
+ onTokenRefreshSuccess,
95
+ onTokenRefreshError
96
+ } = config;
97
+ const axiosInstance = axios2.create({
98
+ baseURL,
99
+ timeout,
100
+ headers: {
101
+ "Content-Type": "application/json",
102
+ Accept: "application/json",
103
+ ...headers
104
+ },
105
+ withCredentials: true
106
+ });
107
+ axiosRetry(axiosInstance, {
108
+ retries: 3,
109
+ retryDelay: axiosRetry.exponentialDelay,
110
+ retryCondition: (error) => axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status === 429
111
+ });
112
+ let isRefreshing = false;
113
+ let requestQueue = [];
114
+ axiosInstance.interceptors.request.use(async (req) => {
115
+ req.headers["X-Request-ID"] = uuidv4();
116
+ if (getAccessToken) {
117
+ const token = await getAccessToken();
118
+ if (token) {
119
+ req.headers.Authorization = `Bearer ${token}`;
120
+ }
121
+ }
122
+ if (req.cancelTokenKey) {
123
+ req.cancelToken = createCancelToken(req.cancelTokenKey).token;
124
+ }
125
+ if (req.method?.toLowerCase() === "get" && req.cache) {
126
+ const cacheKey = req.url || "default-cache-key";
127
+ const cachedData = cacheManager.get(cacheKey);
128
+ if (cachedData) {
129
+ console.log(`[Cache] HIT for ${cacheKey}`);
130
+ req.adapter = () => Promise.resolve({
131
+ data: cachedData,
132
+ status: 200,
133
+ statusText: "OK (from cache)",
134
+ headers: req.headers,
135
+ config: req
136
+ });
137
+ req.cached = true;
138
+ }
139
+ }
140
+ return req;
141
+ }, (error) => Promise.reject(error));
142
+ axiosInstance.interceptors.response.use(
143
+ (response) => {
144
+ const config2 = response.config;
145
+ if (response.config.method?.toLowerCase() === "get" && config2.cache) {
146
+ const cacheKey = response.config.url || "default-cache-key";
147
+ const cacheConfig = typeof config2.cache === "object" ? config2.cache : { enabled: true };
148
+ if (cacheConfig.enabled) {
149
+ console.log(`[Cache] SET for ${cacheKey}`);
150
+ cacheManager.set(cacheKey, response.data, cacheConfig.duration);
151
+ }
152
+ }
153
+ return response;
154
+ },
155
+ async (error) => {
156
+ const originalRequest = error.config;
157
+ if (error.response?.status === 401 && !originalRequest._retry && getRefreshToken && handleTokenRefresh) {
158
+ originalRequest._retry = true;
159
+ if (isRefreshing) {
160
+ return new Promise((resolve) => {
161
+ requestQueue.push((token) => {
162
+ originalRequest.headers.Authorization = `Bearer ${token}`;
163
+ resolve(axiosInstance(originalRequest));
164
+ });
165
+ });
166
+ }
167
+ isRefreshing = true;
168
+ try {
169
+ const currentRefreshToken = await getRefreshToken();
170
+ if (!currentRefreshToken) throw new Error("No refresh token available.");
171
+ const { newAccessToken, newRefreshToken } = await handleTokenRefresh(currentRefreshToken);
172
+ if (onTokenRefreshSuccess) {
173
+ onTokenRefreshSuccess(newAccessToken, newRefreshToken);
174
+ }
175
+ axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`;
176
+ originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
177
+ requestQueue.forEach((cb) => cb(newAccessToken));
178
+ requestQueue = [];
179
+ return axiosInstance(originalRequest);
180
+ } catch (refreshError) {
181
+ if (onTokenRefreshError) {
182
+ onTokenRefreshError(refreshError);
183
+ }
184
+ requestQueue = [];
185
+ return Promise.reject(refreshError);
186
+ } finally {
187
+ isRefreshing = false;
188
+ }
189
+ }
190
+ const enhancedError = {
191
+ message: error.response?.data?.message || error.message || "An unexpected error occurred",
192
+ status: error.response?.status || 500,
193
+ code: error.code,
194
+ errors: error.response?.data?.errors,
195
+ path: originalRequest.url,
196
+ requestId: originalRequest.headers?.["X-Request-ID"]
197
+ };
198
+ return Promise.reject(enhancedError);
199
+ }
200
+ );
201
+ return axiosInstance;
202
+ }
203
+
204
+ // src/core/processor.ts
205
+ var processResponse = (responseOrError) => {
206
+ if (isApiError(responseOrError)) {
207
+ return {
208
+ response: void 0,
209
+ error: responseOrError,
210
+ validationErrors: responseOrError.errors || [],
211
+ success: false,
212
+ loading: false,
213
+ message: responseOrError.message
214
+ };
215
+ }
216
+ if (isAxiosResponse(responseOrError)) {
217
+ const response = responseOrError;
218
+ const isSuccess = response.status >= 200 && response.status < 300;
219
+ if (isSuccess) {
220
+ return {
221
+ response: response.data,
222
+ loading: false,
223
+ success: true,
224
+ cached: response.config.cached,
225
+ error: null,
226
+ validationErrors: [],
227
+ message: response.data.message || "Request successful."
228
+ };
229
+ }
230
+ }
231
+ return {
232
+ response: void 0,
233
+ error: {
234
+ message: "An unknown error occurred during response processing.",
235
+ status: 500
236
+ },
237
+ success: false,
238
+ loading: false
239
+ };
240
+ };
241
+
242
+ // src/services/crud.ts
243
+ function createApiServices(axiosInstance, endpoint) {
244
+ const get = async (id, config) => {
245
+ const url = id ? `${endpoint}/${id}` : endpoint;
246
+ try {
247
+ const response = await axiosInstance.get(url, config);
248
+ return processResponse(response);
249
+ } catch (error) {
250
+ return processResponse(error);
251
+ }
252
+ };
253
+ const getWithQuery = async (query, config) => {
254
+ try {
255
+ const response = await axiosInstance.get(`${endpoint}${query}`, config);
256
+ return processResponse(response);
257
+ } catch (error) {
258
+ return processResponse(error);
259
+ }
260
+ };
261
+ const post = async (data, config) => {
262
+ try {
263
+ const response = await axiosInstance.post(endpoint, data, config);
264
+ return processResponse(response);
265
+ } catch (error) {
266
+ return processResponse(error);
267
+ }
268
+ };
269
+ const patch = async (id, data, config) => {
270
+ try {
271
+ const response = await axiosInstance.patch(`${endpoint}/${id}`, data, config);
272
+ return processResponse(response);
273
+ } catch (error) {
274
+ return processResponse(error);
275
+ }
276
+ };
277
+ const remove = async (id, config) => {
278
+ try {
279
+ const response = await axiosInstance.delete(`${endpoint}/${id}`, config);
280
+ return processResponse(response);
281
+ } catch (error) {
282
+ return processResponse(error);
283
+ }
284
+ };
285
+ return { get, getWithQuery, post, patch, remove };
286
+ }
287
+
288
+ // src/hooks/useApi.ts
289
+ import { useState, useEffect, useCallback, useRef } from "react";
290
+ function useApi(axiosInstance, config) {
291
+ const {
292
+ endpoint,
293
+ initialData,
294
+ initialQuery = { page: 1, limit: 10 },
295
+ enabled = true,
296
+ refetchAfterChange = true,
297
+ onSuccess,
298
+ onError
299
+ } = config;
300
+ const [state, setState] = useState({
301
+ response: initialData || void 0,
302
+ loading: enabled,
303
+ error: null,
304
+ success: false
305
+ });
306
+ const [queryOptions, setQueryOptions] = useState(initialQuery);
307
+ const apiServices = useRef(createApiServices(axiosInstance, endpoint)).current;
308
+ const fetchData = useCallback(async () => {
309
+ setState((prev) => ({ ...prev, loading: true, error: null }));
310
+ const queryString = buildPaginateQuery(queryOptions);
311
+ const result = await apiServices.getWithQuery(queryString, { cancelTokenKey: endpoint });
312
+ setState(result);
313
+ if (!result.success && onError) {
314
+ onError(result.message || "Fetch failed", result.error || void 0);
315
+ }
316
+ }, [apiServices, queryOptions, endpoint, onError]);
317
+ useEffect(() => {
318
+ if (enabled) {
319
+ fetchData();
320
+ }
321
+ }, [enabled, queryOptions]);
322
+ const createItem = async (newItem) => {
323
+ setState((prev) => ({ ...prev, loading: true }));
324
+ const result = await apiServices.post(newItem);
325
+ if (result.success) {
326
+ if (refetchAfterChange) await fetchData();
327
+ else setState((prev) => ({ ...prev, loading: false }));
328
+ if (onSuccess) onSuccess(result.message || "Item created successfully!", result.response);
329
+ } else {
330
+ setState((prev) => ({ ...prev, loading: false, error: result.error }));
331
+ if (onError) onError(result.message || "Create failed", result.error || void 0);
332
+ }
333
+ return result;
334
+ };
335
+ const updateItem = async (id, updatedItem) => {
336
+ setState((prev) => ({ ...prev, loading: true }));
337
+ const result = await apiServices.patch(id, updatedItem);
338
+ if (result.success) {
339
+ if (refetchAfterChange) await fetchData();
340
+ else setState((prev) => ({ ...prev, loading: false }));
341
+ if (onSuccess) onSuccess(result.message || "Item updated successfully!", result.response);
342
+ } else {
343
+ setState((prev) => ({ ...prev, loading: false, error: result.error }));
344
+ if (onError) onError(result.message || "Update failed", result.error || void 0);
345
+ }
346
+ return result;
347
+ };
348
+ const deleteItem = async (id) => {
349
+ setState((prev) => ({ ...prev, loading: true }));
350
+ const result = await apiServices.remove(id);
351
+ if (result.success) {
352
+ if (refetchAfterChange) await fetchData();
353
+ else setState((prev) => ({ ...prev, loading: false }));
354
+ if (onSuccess) onSuccess(result.message || "Item deleted successfully!");
355
+ } else {
356
+ setState((prev) => ({ ...prev, loading: false, error: result.error }));
357
+ if (onError) onError(result.message || "Delete failed", result.error || void 0);
358
+ }
359
+ return result;
360
+ };
361
+ const setPage = (page) => setQueryOptions((prev) => ({ ...prev, page }));
362
+ const setLimit = (limit) => setQueryOptions((prev) => ({ ...prev, limit, page: 1 }));
363
+ const setSearchTerm = (search) => setQueryOptions((prev) => ({ ...prev, search, page: 1 }));
364
+ const setSorting = (sortBy) => setQueryOptions((prev) => ({ ...prev, sortBy }));
365
+ const setFilters = (filter) => setQueryOptions((prev) => ({ ...prev, filter, page: 1 }));
366
+ const resetQuery = () => setQueryOptions(initialQuery);
367
+ return {
368
+ state,
369
+ setState,
370
+ // Exposing setState for direct manipulation or connection to Redux
371
+ actions: {
372
+ fetch: fetchData,
373
+ create: createItem,
374
+ update: updateItem,
375
+ remove: deleteItem
376
+ },
377
+ query: {
378
+ options: queryOptions,
379
+ setOptions: setQueryOptions,
380
+ setPage,
381
+ setLimit,
382
+ setSearchTerm,
383
+ setSorting,
384
+ setFilters,
385
+ reset: resetQuery
386
+ }
387
+ };
388
+ }
389
+ export {
390
+ buildPaginateQuery,
391
+ cacheManager,
392
+ cancelAllRequests,
393
+ cancelRequest,
394
+ createApiClient,
395
+ createApiServices,
396
+ useApi
397
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "api-core-lib",
3
+ "version": "1.0.0",
4
+ "description": "A flexible and powerful API client library for modern web applications.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
10
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch"
11
+ },
12
+ "keywords": [
13
+ "axios",
14
+ "api",
15
+ "react",
16
+ "nextjs",
17
+ "typescript",
18
+ "state-management"
19
+ ],
20
+ "author": "Your Name",
21
+ "license": "MIT",
22
+ "peerDependencies": {
23
+ "react": ">=17.0.0"
24
+ },
25
+ "dependencies": {
26
+ "axios": "^1.6.8",
27
+ "axios-retry": "^4.1.0",
28
+ "uuid": "^9.0.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^18.3.2",
32
+ "@types/uuid": "^9.0.8",
33
+ "react": "^18.3.1",
34
+ "tsup": "^8.0.2",
35
+ "typescript": "^5.4.5"
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ]
40
+ }