@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 CHANGED
@@ -1,5 +1,3 @@
1
- English | [简体中文](./README.zh-CN.md)
2
-
3
1
  # Api Client
4
2
 
5
3
  A lightweight HTTP client built on top of `ofetch`, with:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i.un/api-client",
3
- "version": "0.1.0",
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
  }
@@ -1,8 +0,0 @@
1
- {
2
- "json.schemas": [
3
- {
4
- "url": "https://cdn.jsdelivr.net/npm/tsup/schema.json",
5
- "fileMatch": ["package.json", "tsup.config.json"]
6
- }
7
- ]
8
- }
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
- }
package/tsup.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/client.ts"],
5
- format: ["cjs", "esm"],
6
- dts: true,
7
- sourcemap: true,
8
- clean: true,
9
- external: ["ofetch"],
10
- });