@i.un/api-client 0.1.0 → 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 +0 -2
- package/package.json +4 -1
- package/.vscode/settings.json +0 -8
- package/src/client.ts +0 -263
- package/tsconfig.json +0 -15
- package/tsup.config.ts +0 -10
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i.un/api-client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Universal API client for i.un services",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "nova <www.nova@gmail.com>",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=18"
|
|
47
47
|
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist"
|
|
50
|
+
],
|
|
48
51
|
"publishConfig": {
|
|
49
52
|
"access": "public"
|
|
50
53
|
}
|
package/.vscode/settings.json
DELETED
package/src/client.ts
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ofetch,
|
|
3
|
-
type FetchOptions,
|
|
4
|
-
type FetchContext,
|
|
5
|
-
type $Fetch,
|
|
6
|
-
} from "ofetch";
|
|
7
|
-
|
|
8
|
-
export interface ApiResult<T> {
|
|
9
|
-
code: number;
|
|
10
|
-
data: T;
|
|
11
|
-
message: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ApiError extends Error {
|
|
15
|
-
code: number; // 业务错误码
|
|
16
|
-
data?: unknown; // 后端返回的 data 字段(如果有)
|
|
17
|
-
status?: number; // HTTP 状态码(网络错误时)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// 类型守卫:判断是否是 API 业务错误
|
|
21
|
-
export const isApiError = (error: unknown): error is ApiError => {
|
|
22
|
-
return error instanceof Error && "code" in error;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export interface TokenStorage {
|
|
26
|
-
getAccessToken: () => Promise<string> | string;
|
|
27
|
-
setAccessToken: (token: string) => Promise<void> | void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface CreateApiClientOptions {
|
|
31
|
-
baseURL: string;
|
|
32
|
-
tokenStorage: TokenStorage;
|
|
33
|
-
refreshToken?: (() => Promise<string>) | string | false;
|
|
34
|
-
isAuthError?: (code: number) => boolean;
|
|
35
|
-
unwrapResponse?<T>(result: unknown, returnFullResponse: boolean): T;
|
|
36
|
-
createErrorFromResult?(res: unknown): Error;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type RequestOptions = FetchOptions<"json"> & { returnFullResponse?: boolean };
|
|
40
|
-
|
|
41
|
-
// 解包后端统一响应格式(默认实现)
|
|
42
|
-
const defaultUnwrapResponse = <T>(
|
|
43
|
-
result: unknown,
|
|
44
|
-
returnFullResponse = false
|
|
45
|
-
): T => {
|
|
46
|
-
if (result && typeof result === "object" && "code" in result) {
|
|
47
|
-
const body = result as Record<string, unknown>;
|
|
48
|
-
if (body.code === 0) {
|
|
49
|
-
return returnFullResponse ? (body as T) : (body.data as T);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return result as T;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const defaultCreateErrorFromResult = (res: ApiResult<unknown>): ApiError => {
|
|
56
|
-
const error = new Error(res.message || "Request failed") as ApiError;
|
|
57
|
-
error.code = res.code;
|
|
58
|
-
error.data = res.data;
|
|
59
|
-
return error;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const extractAccessToken = (data: unknown): string => {
|
|
63
|
-
if (typeof data === "string" && data) {
|
|
64
|
-
return data;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!data || typeof data !== "object") {
|
|
68
|
-
throw new Error("Invalid refresh token response: data is not an object or string");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const anyData = data as Record<string, unknown>;
|
|
72
|
-
|
|
73
|
-
const accessToken = anyData.access_token ?? anyData.token;
|
|
74
|
-
|
|
75
|
-
if (typeof accessToken === "string" && accessToken) {
|
|
76
|
-
return accessToken;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
throw new Error(
|
|
80
|
-
"Invalid refresh token response: no access_token or token field found"
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export function createApiClient(options: CreateApiClientOptions) {
|
|
85
|
-
const {
|
|
86
|
-
baseURL,
|
|
87
|
-
tokenStorage,
|
|
88
|
-
refreshToken = false,
|
|
89
|
-
isAuthError = (code: number) => code === 401,
|
|
90
|
-
unwrapResponse = defaultUnwrapResponse,
|
|
91
|
-
createErrorFromResult = defaultCreateErrorFromResult,
|
|
92
|
-
} = options;
|
|
93
|
-
|
|
94
|
-
let refreshingPromise: Promise<string> | null = null;
|
|
95
|
-
|
|
96
|
-
const refreshTokenFn: (() => Promise<string>) | null = !refreshToken
|
|
97
|
-
? null
|
|
98
|
-
: typeof refreshToken === "string"
|
|
99
|
-
? async () => {
|
|
100
|
-
const res = await ofetch<ApiResult<unknown>>(refreshToken, {
|
|
101
|
-
baseURL,
|
|
102
|
-
method: "POST",
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
if (res.code !== 0) {
|
|
106
|
-
throw createErrorFromResult(res as ApiResult<unknown>);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return extractAccessToken(res.data);
|
|
110
|
-
}
|
|
111
|
-
: refreshToken;
|
|
112
|
-
|
|
113
|
-
const rawRequest = ofetch.create({
|
|
114
|
-
baseURL,
|
|
115
|
-
|
|
116
|
-
async onRequest({ options }: FetchContext) {
|
|
117
|
-
const token = await tokenStorage.getAccessToken();
|
|
118
|
-
|
|
119
|
-
const headers = new Headers(options.headers as HeadersInit | undefined);
|
|
120
|
-
|
|
121
|
-
if (token) {
|
|
122
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
options.headers = headers;
|
|
126
|
-
},
|
|
127
|
-
onResponse({ response }) {
|
|
128
|
-
// 不在这里处理 code,统一交给 fetchApi 层处理
|
|
129
|
-
if (response.status === 204) {
|
|
130
|
-
response._data = null;
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
async onResponseError(context: FetchContext) {
|
|
135
|
-
// 后端统一返回 HTTP 200,此处只处理网络层错误(如超时、断网)
|
|
136
|
-
const { response } = context;
|
|
137
|
-
const message =
|
|
138
|
-
(response?._data as Record<string, unknown>)?.message ||
|
|
139
|
-
`HTTP ${response?.status || "Network Error"}`;
|
|
140
|
-
|
|
141
|
-
const error = new Error(message as string) as ApiError;
|
|
142
|
-
error.status = response?.status;
|
|
143
|
-
error.data = response?._data;
|
|
144
|
-
throw error;
|
|
145
|
-
},
|
|
146
|
-
}) as $Fetch;
|
|
147
|
-
|
|
148
|
-
async function request<T = unknown>(
|
|
149
|
-
url: string,
|
|
150
|
-
options: RequestOptions & { _retry?: boolean } = {}
|
|
151
|
-
): Promise<T> {
|
|
152
|
-
// 提取自定义选项,避免传递给 $fetch
|
|
153
|
-
const { returnFullResponse, _retry, ...fetchOptions } = options;
|
|
154
|
-
|
|
155
|
-
const res = await rawRequest<ApiResult<T>>(url, fetchOptions);
|
|
156
|
-
// const res = await rawRequest(url, fetchOptions as RequestOptions);
|
|
157
|
-
|
|
158
|
-
if (res.code === 0) {
|
|
159
|
-
return unwrapResponse<T>(res, !!returnFullResponse);
|
|
160
|
-
// if (returnFullResponse) {
|
|
161
|
-
// return res as unknown as T;
|
|
162
|
-
// }
|
|
163
|
-
|
|
164
|
-
// return res.data;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (isAuthError(res.code) && !_retry && refreshTokenFn) {
|
|
168
|
-
try {
|
|
169
|
-
if (!refreshingPromise) {
|
|
170
|
-
refreshingPromise = refreshTokenFn().finally(() => {
|
|
171
|
-
refreshingPromise = null;
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const newToken = await refreshingPromise;
|
|
176
|
-
|
|
177
|
-
if (newToken) {
|
|
178
|
-
await tokenStorage.setAccessToken(newToken);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return await request<T>(url, {
|
|
182
|
-
...(options || {}),
|
|
183
|
-
_retry: true,
|
|
184
|
-
} as any);
|
|
185
|
-
} catch (e) {
|
|
186
|
-
await tokenStorage.setAccessToken("");
|
|
187
|
-
throw createErrorFromResult(res as ApiResult<unknown>);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
throw createErrorFromResult(res as ApiResult<unknown>);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function get<T = unknown>(
|
|
195
|
-
url: string,
|
|
196
|
-
params: FetchOptions["query"] = {},
|
|
197
|
-
options?: RequestOptions
|
|
198
|
-
) {
|
|
199
|
-
return request<T>(url, {
|
|
200
|
-
...options,
|
|
201
|
-
method: "GET",
|
|
202
|
-
query: params,
|
|
203
|
-
} as any);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function post<T = unknown>(
|
|
207
|
-
url: string,
|
|
208
|
-
body: FetchOptions["body"] = {},
|
|
209
|
-
options?: RequestOptions
|
|
210
|
-
) {
|
|
211
|
-
return request<T>(url, {
|
|
212
|
-
...options,
|
|
213
|
-
method: "POST",
|
|
214
|
-
body,
|
|
215
|
-
} as any);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function put<T = unknown>(
|
|
219
|
-
url: string,
|
|
220
|
-
body: FetchOptions["body"] = {},
|
|
221
|
-
options?: RequestOptions
|
|
222
|
-
) {
|
|
223
|
-
return request<T>(url, {
|
|
224
|
-
...options,
|
|
225
|
-
method: "PUT",
|
|
226
|
-
body,
|
|
227
|
-
} as any);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function patch<T = unknown>(
|
|
231
|
-
url: string,
|
|
232
|
-
body: FetchOptions["body"] = {},
|
|
233
|
-
options?: RequestOptions
|
|
234
|
-
) {
|
|
235
|
-
return request<T>(url, {
|
|
236
|
-
...options,
|
|
237
|
-
method: "PATCH",
|
|
238
|
-
body,
|
|
239
|
-
} as any);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async function del<T = unknown>(
|
|
243
|
-
url: string,
|
|
244
|
-
params: FetchOptions["query"] = {},
|
|
245
|
-
options?: RequestOptions
|
|
246
|
-
) {
|
|
247
|
-
return request<T>(url, {
|
|
248
|
-
...options,
|
|
249
|
-
method: "DELETE",
|
|
250
|
-
query: params,
|
|
251
|
-
} as any);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
rawRequest,
|
|
256
|
-
request,
|
|
257
|
-
get,
|
|
258
|
-
post,
|
|
259
|
-
put,
|
|
260
|
-
patch,
|
|
261
|
-
del,
|
|
262
|
-
};
|
|
263
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"resolveJsonModule": true,
|
|
10
|
-
"allowSyntheticDefaultImports": true,
|
|
11
|
-
"lib": ["ES2020", "DOM"],
|
|
12
|
-
"noEmit": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src"]
|
|
15
|
-
}
|