@amaster.ai/http-client 1.0.0-beta.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) 2026 Amaster Team
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,170 @@
1
+ # @amaster.ai/http-client
2
+
3
+ Base HTTP client with error handling and response unwrapping for backend API communication.
4
+
5
+ ## Features
6
+
7
+ - 🔄 Automatic backend response unwrapping (`{ status: 0, data: {...} }` → `{...}`)
8
+ - 📅 Backend datetime format conversion (`"2026-01-05 10:30:45"` → `"2026-01-05T10:30:45.000Z"`)
9
+ - ⚠️ Consistent error handling with normalized error messages
10
+ - 🔌 Built on Axios with full TypeScript support
11
+ - 🌳 Tree-shakeable ESM/CJS builds
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pnpm add @amaster.ai/http-client axios
17
+ ```
18
+
19
+ > **Note:** `axios` is a peer dependency and must be installed separately.
20
+
21
+ ## Usage
22
+
23
+ ### Basic Usage
24
+
25
+ ```typescript
26
+ import { createHttpClient } from "@amaster.ai/http-client";
27
+
28
+ const client = createHttpClient();
29
+
30
+ const result = await client.request({
31
+ url: "/api/users",
32
+ method: "get",
33
+ });
34
+
35
+ if (result.data) {
36
+ console.log(result.data);
37
+ } else {
38
+ console.error(result.error.message);
39
+ }
40
+ ```
41
+
42
+ ### With Custom Axios Instance
43
+
44
+ ```typescript
45
+ import axios from "axios";
46
+ import { createHttpClient } from "@amaster.ai/http-client";
47
+
48
+ const instance = axios.create({
49
+ baseURL: "https://api.example.com",
50
+ timeout: 5000,
51
+ headers: {
52
+ "Authorization": "Bearer token",
53
+ },
54
+ });
55
+
56
+ const client = createHttpClient(instance);
57
+ ```
58
+
59
+ ### Response Format
60
+
61
+ All requests return a `ClientResult<T>`:
62
+
63
+ ```typescript
64
+ type ClientResult<T> = {
65
+ data: T | null; // Response data (null on error)
66
+ error: ClientError | null; // Error details (null on success)
67
+ status: number; // HTTP status code
68
+ };
69
+ ```
70
+
71
+ ### Error Handling
72
+
73
+ ```typescript
74
+ const result = await client.request({
75
+ url: "/api/users/123",
76
+ method: "get",
77
+ });
78
+
79
+ if (result.error) {
80
+ console.error("Error:", result.error.message);
81
+ console.error("Status:", result.error.status);
82
+ console.error("Details:", result.error.details);
83
+ }
84
+ ```
85
+
86
+ ## Features in Detail
87
+
88
+ ### Backend Response Unwrapping
89
+
90
+ The client automatically unwraps backend standard response format:
91
+
92
+ ```typescript
93
+ // Backend returns:
94
+ {
95
+ "status": 0,
96
+ "data": { "id": 1, "name": "John" }
97
+ }
98
+
99
+ // Client returns:
100
+ {
101
+ data: { "id": 1, "name": "John" },
102
+ error: null,
103
+ status: 200
104
+ }
105
+ ```
106
+
107
+ ### Datetime Conversion
108
+
109
+ Backend datetime strings are automatically converted to ISO 8601 format:
110
+
111
+ ```typescript
112
+ // Backend returns:
113
+ {
114
+ "createdAt": "2026-01-05 10:30:45"
115
+ }
116
+
117
+ // Client returns:
118
+ {
119
+ "createdAt": "2026-01-05T10:30:45.000Z"
120
+ }
121
+ ```
122
+
123
+ This works recursively for nested objects and arrays.
124
+
125
+ ### Error Normalization
126
+
127
+ The client normalizes various backend error formats:
128
+
129
+ ```typescript
130
+ // Backend may return any of:
131
+ { "message": "Error" }
132
+ { "error": "Error" }
133
+ { "msg": "Error" }
134
+ { "detail": "Error" }
135
+
136
+ // Client always returns:
137
+ {
138
+ error: {
139
+ message: "Error",
140
+ status: 400,
141
+ details: { /* original response */ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ ## TypeScript Support
147
+
148
+ Full TypeScript support with generic types:
149
+
150
+ ```typescript
151
+ interface User {
152
+ id: number;
153
+ name: string;
154
+ email: string;
155
+ }
156
+
157
+ const result = await client.request<User>({
158
+ url: "/api/users/1",
159
+ method: "get",
160
+ });
161
+
162
+ if (result.data) {
163
+ // result.data is typed as User
164
+ console.log(result.data.name);
165
+ }
166
+ ```
167
+
168
+ ## License
169
+
170
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ // src/http.ts
4
+ function normalizeErrorMessage(payload) {
5
+ if (!payload) {
6
+ return "Request failed";
7
+ }
8
+ if (typeof payload === "string") {
9
+ return payload;
10
+ }
11
+ if (typeof payload === "object") {
12
+ const errorPayload = payload;
13
+ return errorPayload.message || errorPayload.error || errorPayload.msg || errorPayload.detail || "Request failed";
14
+ }
15
+ return "Request failed";
16
+ }
17
+ function isBackendStandardFormat(data) {
18
+ if (!data || typeof data !== "object" || !("data" in data)) {
19
+ return false;
20
+ }
21
+ if ("status" in data) {
22
+ const status = data.status;
23
+ return status === 0 || status === 1 || status === "0" || status === "1";
24
+ }
25
+ return false;
26
+ }
27
+ function unwrapBackendResponse(responseData) {
28
+ if (isBackendStandardFormat(responseData)) {
29
+ return responseData.data;
30
+ }
31
+ return responseData;
32
+ }
33
+ var BACKEND_DATETIME_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
34
+ function isBackendDatetime(value) {
35
+ return BACKEND_DATETIME_REGEX.test(value);
36
+ }
37
+ function parseBackendDatetime(value) {
38
+ return `${value.replace(" ", "T")}.000Z`;
39
+ }
40
+ function processResponseDates(data) {
41
+ if (data === null || data === void 0) {
42
+ return data;
43
+ }
44
+ if (Array.isArray(data)) {
45
+ return data.map((item) => processResponseDates(item));
46
+ }
47
+ if (typeof data === "object") {
48
+ const processed = {};
49
+ for (const [key, value] of Object.entries(data)) {
50
+ processed[key] = processResponseDates(value);
51
+ }
52
+ return processed;
53
+ }
54
+ if (typeof data === "string" && isBackendDatetime(data)) {
55
+ return parseBackendDatetime(data);
56
+ }
57
+ return data;
58
+ }
59
+ function createHttpClient(axiosInstance) {
60
+ let instance = axiosInstance;
61
+ return {
62
+ async request(config) {
63
+ if (!instance) {
64
+ const axios = await import('axios');
65
+ instance = axios.default.create();
66
+ }
67
+ try {
68
+ const resp = await instance(config);
69
+ const status = resp?.status ?? 0;
70
+ if (status >= 200 && status < 300) {
71
+ const unwrappedData = unwrapBackendResponse(resp?.data);
72
+ const processedData = processResponseDates(unwrappedData);
73
+ return { data: processedData ?? null, error: null, status };
74
+ }
75
+ return {
76
+ data: null,
77
+ error: {
78
+ status,
79
+ message: normalizeErrorMessage(resp?.data) || `HTTP ${status}`,
80
+ details: resp?.data
81
+ },
82
+ status
83
+ };
84
+ } catch (error_) {
85
+ const error = error_;
86
+ const status = error.response?.status || 0;
87
+ return {
88
+ data: null,
89
+ error: {
90
+ status,
91
+ message: normalizeErrorMessage(error.response?.data) || error.message || "Network error",
92
+ details: error.response?.data
93
+ },
94
+ status
95
+ };
96
+ }
97
+ }
98
+ };
99
+ }
100
+
101
+ exports.createHttpClient = createHttpClient;
102
+ //# sourceMappingURL=index.cjs.map
103
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { AxiosInstance, AxiosRequestConfig } from \"axios\";\n\nexport type ClientError = {\n message: string;\n status?: number;\n code?: string;\n details?: unknown;\n};\n\nexport type ClientResult<T> = {\n data: T | null;\n error: ClientError | null;\n status: number;\n};\n\nexport type HttpClient = {\n request<T>(\n config: AxiosRequestConfig & {\n url: string;\n method: NonNullable<AxiosRequestConfig[\"method\"]>;\n }\n ): Promise<ClientResult<T>>;\n};\n\ninterface ErrorPayload {\n message?: string;\n error?: string;\n msg?: string;\n detail?: string;\n}\n\nfunction normalizeErrorMessage(payload: unknown): string {\n if (!payload) {\n return \"Request failed\";\n }\n if (typeof payload === \"string\") {\n return payload;\n }\n if (typeof payload === \"object\") {\n const errorPayload = payload as ErrorPayload;\n return (\n errorPayload.message ||\n errorPayload.error ||\n errorPayload.msg ||\n errorPayload.detail ||\n \"Request failed\"\n );\n }\n return \"Request failed\";\n}\n\ninterface BackendStandardResponse {\n status: number | string;\n data: unknown;\n}\n\n/**\n * Check if response data matches backend standard format: { status: 0/1, data: {...} }\n * Handles both number and string status values\n */\nfunction isBackendStandardFormat(data: unknown): data is BackendStandardResponse {\n if (!data || typeof data !== \"object\" || !(\"data\" in data)) {\n return false;\n }\n\n // Check status field - handle both number and string values\n if (\"status\" in data) {\n const status = (data as BackendStandardResponse).status;\n // Accept 0, 1, \"0\", \"1\" as valid status values\n return status === 0 || status === 1 || status === \"0\" || status === \"1\";\n }\n\n return false;\n}\n\n/**\n * Unwrap backend standard format response\n * Backend returns: { status: 0, data: {...} }\n * We extract the inner 'data' field for cleaner client usage\n */\nfunction unwrapBackendResponse<T>(responseData: unknown): T {\n if (isBackendStandardFormat(responseData)) {\n // Extract inner data field\n return responseData.data as T;\n }\n // Return as-is if not standard format (e.g., BPM/Workflow APIs)\n return responseData as T;\n}\n\n/**\n * Backend datetime format regex: \"YYYY-MM-DD HH:MM:SS\"\n */\nconst BACKEND_DATETIME_REGEX = /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$/;\n\n/**\n * Check if a string matches backend datetime format\n */\nfunction isBackendDatetime(value: string): boolean {\n return BACKEND_DATETIME_REGEX.test(value);\n}\n\n/**\n * Convert backend datetime string to ISO 8601 format\n * \"2026-01-05 10:30:45\" → \"2026-01-05T10:30:45.000Z\"\n */\nfunction parseBackendDatetime(value: string): string {\n // Replace space with T and append Z for UTC\n return `${value.replace(\" \", \"T\")}.000Z`;\n}\n\n/**\n * Recursively process response data to convert backend datetime strings to ISO format\n * Handles nested objects and arrays\n */\nfunction processResponseDates(data: unknown): unknown {\n if (data === null || data === undefined) {\n return data;\n }\n\n // Handle arrays\n if (Array.isArray(data)) {\n return data.map((item) => processResponseDates(item));\n }\n\n // Handle objects\n if (typeof data === \"object\") {\n const processed: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n processed[key] = processResponseDates(value);\n }\n return processed;\n }\n\n // Handle backend datetime strings\n if (typeof data === \"string\" && isBackendDatetime(data)) {\n return parseBackendDatetime(data);\n }\n\n return data;\n}\n\n/**\n * Create an HTTP client instance\n * \n * @param axiosInstance - Optional axios instance to use (defaults to a basic axios instance)\n * @returns HttpClient with request method\n * \n * @example\n * ```typescript\n * import axios from \"axios\";\n * import { createHttpClient } from \"@amaster.ai/http-client\";\n * \n * const instance = axios.create({ baseURL: \"https://api.example.com\" });\n * const client = createHttpClient(instance);\n * \n * const result = await client.request({\n * url: \"/users\",\n * method: \"get\",\n * });\n * ```\n */\nexport function createHttpClient(axiosInstance?: AxiosInstance): HttpClient {\n // Import axios dynamically to avoid bundling it\n let instance = axiosInstance;\n \n return {\n async request<T>(\n config: AxiosRequestConfig & {\n url: string;\n method: NonNullable<AxiosRequestConfig[\"method\"]>;\n }\n ) {\n // Lazy load axios if not provided\n if (!instance) {\n const axios = await import(\"axios\");\n instance = axios.default.create();\n }\n\n try {\n const resp = await instance(config);\n const status = resp?.status ?? 0;\n\n if (status >= 200 && status < 300) {\n // Unwrap backend standard format: { status: 0, data: {...} } -> {...}\n const unwrappedData = unwrapBackendResponse<T>(resp?.data);\n // Convert backend datetime strings to ISO format\n const processedData = processResponseDates(unwrappedData);\n return { data: (processedData ?? null) as T | null, error: null, status };\n }\n\n return {\n data: null,\n error: {\n status,\n message: normalizeErrorMessage(resp?.data) || `HTTP ${status}`,\n details: resp?.data,\n },\n status,\n };\n } catch (error_: unknown) {\n // Catch network errors, timeouts, and other axios exceptions\n const error = error_ as {\n response?: { status?: number; data?: unknown };\n message?: string;\n };\n const status = error.response?.status || 0;\n return {\n data: null,\n error: {\n status,\n message:\n normalizeErrorMessage(error.response?.data) || error.message || \"Network error\",\n details: error.response?.data,\n },\n status,\n };\n }\n },\n };\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import { AxiosRequestConfig, AxiosInstance } from 'axios';
2
+
3
+ type ClientError = {
4
+ message: string;
5
+ status?: number;
6
+ code?: string;
7
+ details?: unknown;
8
+ };
9
+ type ClientResult<T> = {
10
+ data: T | null;
11
+ error: ClientError | null;
12
+ status: number;
13
+ };
14
+ type HttpClient = {
15
+ request<T>(config: AxiosRequestConfig & {
16
+ url: string;
17
+ method: NonNullable<AxiosRequestConfig["method"]>;
18
+ }): Promise<ClientResult<T>>;
19
+ };
20
+ /**
21
+ * Create an HTTP client instance
22
+ *
23
+ * @param axiosInstance - Optional axios instance to use (defaults to a basic axios instance)
24
+ * @returns HttpClient with request method
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import axios from "axios";
29
+ * import { createHttpClient } from "@amaster.ai/http-client";
30
+ *
31
+ * const instance = axios.create({ baseURL: "https://api.example.com" });
32
+ * const client = createHttpClient(instance);
33
+ *
34
+ * const result = await client.request({
35
+ * url: "/users",
36
+ * method: "get",
37
+ * });
38
+ * ```
39
+ */
40
+ declare function createHttpClient(axiosInstance?: AxiosInstance): HttpClient;
41
+
42
+ export { type ClientError, type ClientResult, type HttpClient, createHttpClient };
@@ -0,0 +1,42 @@
1
+ import { AxiosRequestConfig, AxiosInstance } from 'axios';
2
+
3
+ type ClientError = {
4
+ message: string;
5
+ status?: number;
6
+ code?: string;
7
+ details?: unknown;
8
+ };
9
+ type ClientResult<T> = {
10
+ data: T | null;
11
+ error: ClientError | null;
12
+ status: number;
13
+ };
14
+ type HttpClient = {
15
+ request<T>(config: AxiosRequestConfig & {
16
+ url: string;
17
+ method: NonNullable<AxiosRequestConfig["method"]>;
18
+ }): Promise<ClientResult<T>>;
19
+ };
20
+ /**
21
+ * Create an HTTP client instance
22
+ *
23
+ * @param axiosInstance - Optional axios instance to use (defaults to a basic axios instance)
24
+ * @returns HttpClient with request method
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import axios from "axios";
29
+ * import { createHttpClient } from "@amaster.ai/http-client";
30
+ *
31
+ * const instance = axios.create({ baseURL: "https://api.example.com" });
32
+ * const client = createHttpClient(instance);
33
+ *
34
+ * const result = await client.request({
35
+ * url: "/users",
36
+ * method: "get",
37
+ * });
38
+ * ```
39
+ */
40
+ declare function createHttpClient(axiosInstance?: AxiosInstance): HttpClient;
41
+
42
+ export { type ClientError, type ClientResult, type HttpClient, createHttpClient };
package/dist/index.js ADDED
@@ -0,0 +1,101 @@
1
+ // src/http.ts
2
+ function normalizeErrorMessage(payload) {
3
+ if (!payload) {
4
+ return "Request failed";
5
+ }
6
+ if (typeof payload === "string") {
7
+ return payload;
8
+ }
9
+ if (typeof payload === "object") {
10
+ const errorPayload = payload;
11
+ return errorPayload.message || errorPayload.error || errorPayload.msg || errorPayload.detail || "Request failed";
12
+ }
13
+ return "Request failed";
14
+ }
15
+ function isBackendStandardFormat(data) {
16
+ if (!data || typeof data !== "object" || !("data" in data)) {
17
+ return false;
18
+ }
19
+ if ("status" in data) {
20
+ const status = data.status;
21
+ return status === 0 || status === 1 || status === "0" || status === "1";
22
+ }
23
+ return false;
24
+ }
25
+ function unwrapBackendResponse(responseData) {
26
+ if (isBackendStandardFormat(responseData)) {
27
+ return responseData.data;
28
+ }
29
+ return responseData;
30
+ }
31
+ var BACKEND_DATETIME_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
32
+ function isBackendDatetime(value) {
33
+ return BACKEND_DATETIME_REGEX.test(value);
34
+ }
35
+ function parseBackendDatetime(value) {
36
+ return `${value.replace(" ", "T")}.000Z`;
37
+ }
38
+ function processResponseDates(data) {
39
+ if (data === null || data === void 0) {
40
+ return data;
41
+ }
42
+ if (Array.isArray(data)) {
43
+ return data.map((item) => processResponseDates(item));
44
+ }
45
+ if (typeof data === "object") {
46
+ const processed = {};
47
+ for (const [key, value] of Object.entries(data)) {
48
+ processed[key] = processResponseDates(value);
49
+ }
50
+ return processed;
51
+ }
52
+ if (typeof data === "string" && isBackendDatetime(data)) {
53
+ return parseBackendDatetime(data);
54
+ }
55
+ return data;
56
+ }
57
+ function createHttpClient(axiosInstance) {
58
+ let instance = axiosInstance;
59
+ return {
60
+ async request(config) {
61
+ if (!instance) {
62
+ const axios = await import('axios');
63
+ instance = axios.default.create();
64
+ }
65
+ try {
66
+ const resp = await instance(config);
67
+ const status = resp?.status ?? 0;
68
+ if (status >= 200 && status < 300) {
69
+ const unwrappedData = unwrapBackendResponse(resp?.data);
70
+ const processedData = processResponseDates(unwrappedData);
71
+ return { data: processedData ?? null, error: null, status };
72
+ }
73
+ return {
74
+ data: null,
75
+ error: {
76
+ status,
77
+ message: normalizeErrorMessage(resp?.data) || `HTTP ${status}`,
78
+ details: resp?.data
79
+ },
80
+ status
81
+ };
82
+ } catch (error_) {
83
+ const error = error_;
84
+ const status = error.response?.status || 0;
85
+ return {
86
+ data: null,
87
+ error: {
88
+ status,
89
+ message: normalizeErrorMessage(error.response?.data) || error.message || "Network error",
90
+ details: error.response?.data
91
+ },
92
+ status
93
+ };
94
+ }
95
+ }
96
+ };
97
+ }
98
+
99
+ export { createHttpClient };
100
+ //# sourceMappingURL=index.js.map
101
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { AxiosInstance, AxiosRequestConfig } from \"axios\";\n\nexport type ClientError = {\n message: string;\n status?: number;\n code?: string;\n details?: unknown;\n};\n\nexport type ClientResult<T> = {\n data: T | null;\n error: ClientError | null;\n status: number;\n};\n\nexport type HttpClient = {\n request<T>(\n config: AxiosRequestConfig & {\n url: string;\n method: NonNullable<AxiosRequestConfig[\"method\"]>;\n }\n ): Promise<ClientResult<T>>;\n};\n\ninterface ErrorPayload {\n message?: string;\n error?: string;\n msg?: string;\n detail?: string;\n}\n\nfunction normalizeErrorMessage(payload: unknown): string {\n if (!payload) {\n return \"Request failed\";\n }\n if (typeof payload === \"string\") {\n return payload;\n }\n if (typeof payload === \"object\") {\n const errorPayload = payload as ErrorPayload;\n return (\n errorPayload.message ||\n errorPayload.error ||\n errorPayload.msg ||\n errorPayload.detail ||\n \"Request failed\"\n );\n }\n return \"Request failed\";\n}\n\ninterface BackendStandardResponse {\n status: number | string;\n data: unknown;\n}\n\n/**\n * Check if response data matches backend standard format: { status: 0/1, data: {...} }\n * Handles both number and string status values\n */\nfunction isBackendStandardFormat(data: unknown): data is BackendStandardResponse {\n if (!data || typeof data !== \"object\" || !(\"data\" in data)) {\n return false;\n }\n\n // Check status field - handle both number and string values\n if (\"status\" in data) {\n const status = (data as BackendStandardResponse).status;\n // Accept 0, 1, \"0\", \"1\" as valid status values\n return status === 0 || status === 1 || status === \"0\" || status === \"1\";\n }\n\n return false;\n}\n\n/**\n * Unwrap backend standard format response\n * Backend returns: { status: 0, data: {...} }\n * We extract the inner 'data' field for cleaner client usage\n */\nfunction unwrapBackendResponse<T>(responseData: unknown): T {\n if (isBackendStandardFormat(responseData)) {\n // Extract inner data field\n return responseData.data as T;\n }\n // Return as-is if not standard format (e.g., BPM/Workflow APIs)\n return responseData as T;\n}\n\n/**\n * Backend datetime format regex: \"YYYY-MM-DD HH:MM:SS\"\n */\nconst BACKEND_DATETIME_REGEX = /^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$/;\n\n/**\n * Check if a string matches backend datetime format\n */\nfunction isBackendDatetime(value: string): boolean {\n return BACKEND_DATETIME_REGEX.test(value);\n}\n\n/**\n * Convert backend datetime string to ISO 8601 format\n * \"2026-01-05 10:30:45\" → \"2026-01-05T10:30:45.000Z\"\n */\nfunction parseBackendDatetime(value: string): string {\n // Replace space with T and append Z for UTC\n return `${value.replace(\" \", \"T\")}.000Z`;\n}\n\n/**\n * Recursively process response data to convert backend datetime strings to ISO format\n * Handles nested objects and arrays\n */\nfunction processResponseDates(data: unknown): unknown {\n if (data === null || data === undefined) {\n return data;\n }\n\n // Handle arrays\n if (Array.isArray(data)) {\n return data.map((item) => processResponseDates(item));\n }\n\n // Handle objects\n if (typeof data === \"object\") {\n const processed: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n processed[key] = processResponseDates(value);\n }\n return processed;\n }\n\n // Handle backend datetime strings\n if (typeof data === \"string\" && isBackendDatetime(data)) {\n return parseBackendDatetime(data);\n }\n\n return data;\n}\n\n/**\n * Create an HTTP client instance\n * \n * @param axiosInstance - Optional axios instance to use (defaults to a basic axios instance)\n * @returns HttpClient with request method\n * \n * @example\n * ```typescript\n * import axios from \"axios\";\n * import { createHttpClient } from \"@amaster.ai/http-client\";\n * \n * const instance = axios.create({ baseURL: \"https://api.example.com\" });\n * const client = createHttpClient(instance);\n * \n * const result = await client.request({\n * url: \"/users\",\n * method: \"get\",\n * });\n * ```\n */\nexport function createHttpClient(axiosInstance?: AxiosInstance): HttpClient {\n // Import axios dynamically to avoid bundling it\n let instance = axiosInstance;\n \n return {\n async request<T>(\n config: AxiosRequestConfig & {\n url: string;\n method: NonNullable<AxiosRequestConfig[\"method\"]>;\n }\n ) {\n // Lazy load axios if not provided\n if (!instance) {\n const axios = await import(\"axios\");\n instance = axios.default.create();\n }\n\n try {\n const resp = await instance(config);\n const status = resp?.status ?? 0;\n\n if (status >= 200 && status < 300) {\n // Unwrap backend standard format: { status: 0, data: {...} } -> {...}\n const unwrappedData = unwrapBackendResponse<T>(resp?.data);\n // Convert backend datetime strings to ISO format\n const processedData = processResponseDates(unwrappedData);\n return { data: (processedData ?? null) as T | null, error: null, status };\n }\n\n return {\n data: null,\n error: {\n status,\n message: normalizeErrorMessage(resp?.data) || `HTTP ${status}`,\n details: resp?.data,\n },\n status,\n };\n } catch (error_: unknown) {\n // Catch network errors, timeouts, and other axios exceptions\n const error = error_ as {\n response?: { status?: number; data?: unknown };\n message?: string;\n };\n const status = error.response?.status || 0;\n return {\n data: null,\n error: {\n status,\n message:\n normalizeErrorMessage(error.response?.data) || error.message || \"Network error\",\n details: error.response?.data,\n },\n status,\n };\n }\n },\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@amaster.ai/http-client",
3
+ "version": "1.0.0-beta.0",
4
+ "description": "Base HTTP client with error handling and response unwrapping",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "http",
22
+ "client",
23
+ "axios",
24
+ "api",
25
+ "typescript"
26
+ ],
27
+ "author": "Amaster Team",
28
+ "license": "MIT",
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ },
33
+ "peerDependencies": {
34
+ "axios": "^1.11.0"
35
+ },
36
+ "devDependencies": {
37
+ "axios": "^1.11.0",
38
+ "tsup": "^8.3.5",
39
+ "typescript": "~5.7.2"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "dev": "tsup --watch",
44
+ "clean": "rm -rf dist *.tsbuildinfo",
45
+ "type-check": "tsc --noEmit"
46
+ }
47
+ }