@ctil/gql 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/.vscode/launch.json +18 -0
- package/README.md +248 -0
- package/cc-request-1.0.0.tgz +0 -0
- package/dist/fp.esm-VY6KF7TP.js +2699 -0
- package/dist/fp.esm-VY6KF7TP.js.map +1 -0
- package/dist/index.cjs +4632 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +300 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +1865 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/src/builders/auth.ts +182 -0
- package/src/builders/baseType.ts +194 -0
- package/src/builders/index.ts +5 -0
- package/src/builders/mutation.ts +341 -0
- package/src/builders/query.ts +180 -0
- package/src/builders/sms.ts +59 -0
- package/src/cache/memoryCache.ts +34 -0
- package/src/core/api/auth.ts +86 -0
- package/src/core/api/gql.ts +22 -0
- package/src/core/api/mutation.ts +100 -0
- package/src/core/api/query.ts +82 -0
- package/src/core/api/sms.ts +18 -0
- package/src/core/client.ts +47 -0
- package/src/core/core.ts +281 -0
- package/src/core/executor.ts +19 -0
- package/src/core/type.ts +76 -0
- package/src/device/index.ts +116 -0
- package/src/index.ts +60 -0
- package/src/rateLimit/rateLimit.ts +51 -0
- package/src/rateLimit/rateLimitConfig.ts +12 -0
- package/src/test.ts +80 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +10 -0
package/src/core/core.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { GraphQLClient } from "graphql-request";
|
|
2
|
+
import { print, DocumentNode } from "graphql";
|
|
3
|
+
import { RequestConfig, RequestInterceptor, UserToken } from "./type.ts";
|
|
4
|
+
import { getDeviceInfo } from "../device/index.ts";
|
|
5
|
+
import { auth } from './api/auth.ts'
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
// 环境判断
|
|
10
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
11
|
+
const isNode = typeof process !== 'undefined' && process.versions?.node != null;
|
|
12
|
+
|
|
13
|
+
// Node.js 文件存储路径
|
|
14
|
+
const NODE_LOGIN_FILE = path.resolve(process.cwd(), "loginInfo.json");
|
|
15
|
+
const STORAGE_KEY = "GRAPHQL_LOGIN_INFO";
|
|
16
|
+
|
|
17
|
+
// Node 内存存储
|
|
18
|
+
let nodeLoginInfo: UserToken | null = null;
|
|
19
|
+
|
|
20
|
+
export class CCRequest {
|
|
21
|
+
private client!: GraphQLClient;
|
|
22
|
+
private headers: Record<string, string>;
|
|
23
|
+
private deviceInfoPromise: Promise<{ deviceId: string; deviceName: string }>;
|
|
24
|
+
private interceptors: RequestInterceptor[] = [];
|
|
25
|
+
|
|
26
|
+
constructor(private config: RequestConfig) {
|
|
27
|
+
this.deviceInfoPromise = getDeviceInfo();
|
|
28
|
+
this.headers = this.buildHeaders(config);
|
|
29
|
+
|
|
30
|
+
// 尝试恢复登录信息
|
|
31
|
+
const loginInfo = this.loadLoginInfo();
|
|
32
|
+
if (loginInfo?.token) this.setToken(loginInfo.token);
|
|
33
|
+
|
|
34
|
+
this.buildClient();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 注册一个拦截器(支持多个) */
|
|
38
|
+
use(interceptor: RequestInterceptor) {
|
|
39
|
+
this.interceptors.push(interceptor);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 构建 GraphQLClient 实例 */
|
|
43
|
+
private buildClient() {
|
|
44
|
+
this.client = new GraphQLClient(this.config.endpoint || "/graphql", {
|
|
45
|
+
headers: this.headers,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 构建初始 headers */
|
|
50
|
+
private buildHeaders(config: RequestConfig): Record<string, string> {
|
|
51
|
+
const headers: Record<string, string> = {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
...config.headers,
|
|
54
|
+
};
|
|
55
|
+
if (config.Authorization) {
|
|
56
|
+
headers.Authorization = `Bearer ${config.Authorization}`;
|
|
57
|
+
}
|
|
58
|
+
return headers;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ===== Token 管理 =====
|
|
62
|
+
setToken(token: string) {
|
|
63
|
+
this.config.Authorization = token;
|
|
64
|
+
this.headers.Authorization = `Bearer ${token}`;
|
|
65
|
+
this.buildClient();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeToken() {
|
|
69
|
+
this.config.Authorization = undefined;
|
|
70
|
+
delete this.headers.Authorization;
|
|
71
|
+
this.buildClient();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ===== LoginInfo 管理 =====
|
|
75
|
+
setLoginInfo(loginInfo: UserToken, remember: boolean = false) {
|
|
76
|
+
this.config.loginInfo = loginInfo;
|
|
77
|
+
if (loginInfo.token) this.setToken(loginInfo.token);
|
|
78
|
+
|
|
79
|
+
if (isBrowser) {
|
|
80
|
+
const storage = remember ? localStorage : sessionStorage;
|
|
81
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(loginInfo));
|
|
82
|
+
} else if (isNode) {
|
|
83
|
+
nodeLoginInfo = loginInfo;
|
|
84
|
+
try {
|
|
85
|
+
fs.writeFileSync(NODE_LOGIN_FILE, JSON.stringify(loginInfo), "utf-8");
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error("Failed to persist login info to file:", err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 加载登录信息(含访问 token 和刷新 token 过期检查) */
|
|
93
|
+
/** 加载登录信息(仅负责加载,不负责刷新) */
|
|
94
|
+
loadLoginInfo(): UserToken | null {
|
|
95
|
+
let info: UserToken | null = null;
|
|
96
|
+
|
|
97
|
+
if (isBrowser) {
|
|
98
|
+
const str = localStorage.getItem(STORAGE_KEY) || sessionStorage.getItem(STORAGE_KEY);
|
|
99
|
+
if (str) {
|
|
100
|
+
try {
|
|
101
|
+
info = JSON.parse(str);
|
|
102
|
+
} catch {
|
|
103
|
+
info = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (isNode) {
|
|
107
|
+
if (nodeLoginInfo) return nodeLoginInfo;
|
|
108
|
+
if (fs.existsSync(NODE_LOGIN_FILE)) {
|
|
109
|
+
try {
|
|
110
|
+
const raw = fs.readFileSync(NODE_LOGIN_FILE, "utf-8");
|
|
111
|
+
info = JSON.parse(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
info = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 🚫 不在这里判断过期,也不删除
|
|
119
|
+
if (info) this.config.loginInfo = info;
|
|
120
|
+
return info;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
getLoginInfo(): UserToken | null {
|
|
125
|
+
if (this.config.loginInfo) return this.config.loginInfo;
|
|
126
|
+
return this.loadLoginInfo();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
removeLoginInfo() {
|
|
130
|
+
delete this.config.loginInfo;
|
|
131
|
+
if (isBrowser) {
|
|
132
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
133
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
134
|
+
} else if (isNode) {
|
|
135
|
+
nodeLoginInfo = null;
|
|
136
|
+
try {
|
|
137
|
+
// if (fs.existsSync(NODE_LOGIN_FILE)) fs.unlinkSync(NODE_LOGIN_FILE);
|
|
138
|
+
} catch { }
|
|
139
|
+
}
|
|
140
|
+
this.removeToken();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ===== Endpoint & Header 管理 =====
|
|
144
|
+
setEndpoint(endpoint: string) {
|
|
145
|
+
this.config.endpoint = endpoint;
|
|
146
|
+
this.buildClient();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setHeader(key: string, value: string) {
|
|
150
|
+
this.headers[key] = value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setHeaders(headers: Record<string, string>) {
|
|
154
|
+
Object.assign(this.headers, headers);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
removeHeader(key: string) {
|
|
158
|
+
delete this.headers[key];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
clearHeaders() {
|
|
162
|
+
this.headers = { "Content-Type": "application/json" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
//无感刷新
|
|
167
|
+
private async ensureTokenValid(): Promise<void> {
|
|
168
|
+
const loginInfo = this.getLoginInfo();
|
|
169
|
+
if (!loginInfo) return;
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
const accessExpired = new Date(loginInfo.expireAt).getTime() <= now;
|
|
172
|
+
const refreshExpired = new Date(loginInfo.refreshExpireAt).getTime() <= now;
|
|
173
|
+
if (refreshExpired) {
|
|
174
|
+
// refreshToken 也过期,直接登出
|
|
175
|
+
this.removeLoginInfo();
|
|
176
|
+
throw new Error("Login expired. Please login again.");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (accessExpired && !refreshExpired) {
|
|
180
|
+
try {
|
|
181
|
+
const refreshResult = await auth.refreshToken({
|
|
182
|
+
refreshToken: loginInfo.refreshToken,
|
|
183
|
+
remember: true,
|
|
184
|
+
});
|
|
185
|
+
// ✅ 确保这里取到完整的 token 对象
|
|
186
|
+
const newInfo: UserToken = (refreshResult as any).refreshToken ?? refreshResult;
|
|
187
|
+
|
|
188
|
+
// 更新登录信息并覆盖原来的 headers
|
|
189
|
+
this.setLoginInfo(newInfo, true);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
this.removeLoginInfo();
|
|
192
|
+
throw new Error("Failed to refresh token. Please login again.");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ===== 请求逻辑 =====
|
|
199
|
+
async request<T = any>(
|
|
200
|
+
query: string | DocumentNode,
|
|
201
|
+
variables?: Record<string, any>
|
|
202
|
+
): Promise<T> {
|
|
203
|
+
let queryStr = typeof query === "string" ? query : print(query);
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
// 🚫 如果是 refreshToken 请求,跳过 token 校验,避免死循环
|
|
207
|
+
if (!/refreshToken/i.test(queryStr)) {
|
|
208
|
+
await this.ensureTokenValid();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
const { deviceId, deviceName } = await this.deviceInfoPromise;
|
|
213
|
+
|
|
214
|
+
let headersWithDevice: Record<string, string> = Object.fromEntries(
|
|
215
|
+
Object.entries({
|
|
216
|
+
...this.headers,
|
|
217
|
+
"X-Device-Id": deviceId,
|
|
218
|
+
"X-Device-Name": deviceName,
|
|
219
|
+
}).filter(([_, v]) => v !== undefined)
|
|
220
|
+
) as Record<string, string>;
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
// === 执行所有 onRequest 拦截器 ===
|
|
225
|
+
for (const interceptor of this.interceptors) {
|
|
226
|
+
if (interceptor.onRequest) {
|
|
227
|
+
const result = await interceptor.onRequest({
|
|
228
|
+
query: queryStr,
|
|
229
|
+
variables,
|
|
230
|
+
headers: headersWithDevice,
|
|
231
|
+
});
|
|
232
|
+
queryStr = result.query;
|
|
233
|
+
variables = result.variables;
|
|
234
|
+
headersWithDevice = Object.fromEntries(
|
|
235
|
+
Object.entries(result.headers).filter(([_, v]) => v !== undefined)
|
|
236
|
+
) as Record<string, string>;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const res = await this.client.rawRequest<T>(
|
|
242
|
+
queryStr,
|
|
243
|
+
variables,
|
|
244
|
+
headersWithDevice
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
let data = res.data;
|
|
248
|
+
|
|
249
|
+
// === 执行所有 onResponse 拦截器 ===
|
|
250
|
+
for (const interceptor of this.interceptors) {
|
|
251
|
+
if (interceptor.onResponse) {
|
|
252
|
+
data = await interceptor.onResponse(data);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return data;
|
|
257
|
+
} catch (err: any) {
|
|
258
|
+
const message = err.response?.errors?.[0]?.message ?? err.message;
|
|
259
|
+
const status = err.response?.errors?.[0]?.extensions?.code ?? 500;
|
|
260
|
+
|
|
261
|
+
const formattedError = {
|
|
262
|
+
message,
|
|
263
|
+
status,
|
|
264
|
+
data: err.response?.data ?? null,
|
|
265
|
+
errors: err.response?.errors ?? null,
|
|
266
|
+
request: err.request,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// === 执行所有 onError 拦截器 ===
|
|
270
|
+
for (const interceptor of this.interceptors) {
|
|
271
|
+
if (interceptor.onError) {
|
|
272
|
+
await interceptor.onError(formattedError);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw formattedError;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/core/executor.ts
|
|
2
|
+
import { getClient } from './client.ts';
|
|
3
|
+
import type { DocumentNode } from 'graphql';
|
|
4
|
+
|
|
5
|
+
interface GraphQLPayload {
|
|
6
|
+
query: string | DocumentNode;
|
|
7
|
+
variables?: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 通用执行函数
|
|
12
|
+
*/
|
|
13
|
+
export async function execute<T = any>(payload: GraphQLPayload) {
|
|
14
|
+
const client = getClient();
|
|
15
|
+
return client.request<T>(payload.query, payload.variables);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
package/src/core/type.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface RequestConfig {
|
|
2
|
+
endpoint?: string;
|
|
3
|
+
Authorization?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
loginInfo?:UserToken | null | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** 拦截器接口 */
|
|
8
|
+
export interface RequestInterceptor {
|
|
9
|
+
/** 请求发出前拦截,可修改 query、variables、headers */
|
|
10
|
+
onRequest?: (params: {
|
|
11
|
+
query: string;
|
|
12
|
+
variables?: Record<string, any>;
|
|
13
|
+
headers: Record<string, string | undefined> & {
|
|
14
|
+
"X-Device-Id"?: string;
|
|
15
|
+
"X-Device-Name"?: string;
|
|
16
|
+
};
|
|
17
|
+
}) => Promise<typeof params> | typeof params;
|
|
18
|
+
|
|
19
|
+
/** 响应成功拦截,可修改返回结构 */
|
|
20
|
+
onResponse?: (response: any) => Promise<any> | any;
|
|
21
|
+
|
|
22
|
+
/** 错误拦截,可做统一错误处理或重试 */
|
|
23
|
+
onError?: (error: any) => Promise<any> | any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export interface requestResult<T = any> {
|
|
28
|
+
[key: string]: T
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export interface UserToken {
|
|
33
|
+
/** 用户 ID */
|
|
34
|
+
userId: number;
|
|
35
|
+
|
|
36
|
+
/** 登录账号 */
|
|
37
|
+
loginAccount: string;
|
|
38
|
+
|
|
39
|
+
/** 访问 Token */
|
|
40
|
+
token: string;
|
|
41
|
+
|
|
42
|
+
/** 刷新 Token */
|
|
43
|
+
refreshToken: string;
|
|
44
|
+
|
|
45
|
+
/** 访问 Token 过期时间 */
|
|
46
|
+
expireAt: string; // ISO 时间字符串
|
|
47
|
+
|
|
48
|
+
/** 刷新 Token 过期时间 */
|
|
49
|
+
refreshExpireAt: string;
|
|
50
|
+
|
|
51
|
+
/** 设备 ID */
|
|
52
|
+
deviceId?: string;
|
|
53
|
+
|
|
54
|
+
/** 设备名称 */
|
|
55
|
+
deviceName?: string;
|
|
56
|
+
|
|
57
|
+
/** 登录时间 */
|
|
58
|
+
loginTime?: string;
|
|
59
|
+
|
|
60
|
+
/** 登录 IP */
|
|
61
|
+
requestIp?: string;
|
|
62
|
+
|
|
63
|
+
/** 客户端信息 */
|
|
64
|
+
userAgent?: string;
|
|
65
|
+
|
|
66
|
+
/** 上次刷新时间 */
|
|
67
|
+
lastRefreshTime?: string;
|
|
68
|
+
|
|
69
|
+
/** 用户角色编码列表 */
|
|
70
|
+
roles: string[];
|
|
71
|
+
|
|
72
|
+
/** 用户权限编码列表 */
|
|
73
|
+
permissions: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
|
|
3
|
+
export interface DeviceInfo {
|
|
4
|
+
deviceId: string;
|
|
5
|
+
deviceName: string;
|
|
6
|
+
env: "node" | "browser";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** 浏览器 IndexedDB 存取 deviceId */
|
|
10
|
+
function getDeviceIdFromIndexedDB(): Promise<string | null> {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
if (typeof indexedDB === "undefined") return resolve(null);
|
|
13
|
+
|
|
14
|
+
const request = indexedDB.open("DeviceDB", 1);
|
|
15
|
+
request.onupgradeneeded = () => {
|
|
16
|
+
const db = request.result;
|
|
17
|
+
db.createObjectStore("info");
|
|
18
|
+
};
|
|
19
|
+
request.onsuccess = () => {
|
|
20
|
+
const db = request.result;
|
|
21
|
+
const tx = db.transaction("info", "readonly");
|
|
22
|
+
const store = tx.objectStore("info");
|
|
23
|
+
const getReq = store.get("deviceId");
|
|
24
|
+
getReq.onsuccess = () => resolve(getReq.result ?? null);
|
|
25
|
+
getReq.onerror = () => resolve(null);
|
|
26
|
+
};
|
|
27
|
+
request.onerror = () => resolve(null);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setDeviceIdToIndexedDB(id: string): Promise<void> {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
if (typeof indexedDB === "undefined") return resolve();
|
|
34
|
+
|
|
35
|
+
const request = indexedDB.open("DeviceDB", 1);
|
|
36
|
+
request.onupgradeneeded = () => {
|
|
37
|
+
const db = request.result;
|
|
38
|
+
db.createObjectStore("info");
|
|
39
|
+
};
|
|
40
|
+
request.onsuccess = () => {
|
|
41
|
+
const db = request.result;
|
|
42
|
+
const tx = db.transaction("info", "readwrite");
|
|
43
|
+
const store = tx.objectStore("info");
|
|
44
|
+
store.put(id, "deviceId");
|
|
45
|
+
tx.oncomplete = () => resolve();
|
|
46
|
+
tx.onerror = () => resolve();
|
|
47
|
+
};
|
|
48
|
+
request.onerror = () => resolve();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 浏览器端设备信息 */
|
|
53
|
+
export async function getBrowserDeviceInfo(): Promise<DeviceInfo> {
|
|
54
|
+
let deviceId: string;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const { load } = await import("@fingerprintjs/fingerprintjs");
|
|
58
|
+
const fp = await load();
|
|
59
|
+
const result = await fp.get();
|
|
60
|
+
deviceId = result.visitorId;
|
|
61
|
+
} catch {
|
|
62
|
+
// FingerprintJS 不可用,使用 IndexedDB + UUID
|
|
63
|
+
let id = await getDeviceIdFromIndexedDB();
|
|
64
|
+
if (!id) {
|
|
65
|
+
id = uuidv4();
|
|
66
|
+
await setDeviceIdToIndexedDB(id);
|
|
67
|
+
}
|
|
68
|
+
deviceId = id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 生成 deviceName,不依赖 userAgent
|
|
72
|
+
const deviceName = (() => {
|
|
73
|
+
const platform = navigator.platform || "unknown";
|
|
74
|
+
const width = screen.width || 0;
|
|
75
|
+
const height = screen.height || 0;
|
|
76
|
+
const colorDepth = screen.colorDepth || 24;
|
|
77
|
+
return `${platform}-${width}x${height}x${colorDepth}`;
|
|
78
|
+
})();
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
deviceId,
|
|
82
|
+
deviceName,
|
|
83
|
+
env: "browser",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Node 端设备信息 */
|
|
88
|
+
async function getNodeDeviceInfo(): Promise<DeviceInfo> {
|
|
89
|
+
const os = await import("os");
|
|
90
|
+
const machine = await import("node-machine-id");
|
|
91
|
+
|
|
92
|
+
const machineIdSync =
|
|
93
|
+
(machine as any).machineIdSync ||
|
|
94
|
+
(machine as any).default?.machineIdSync;
|
|
95
|
+
|
|
96
|
+
if (typeof machineIdSync !== "function") {
|
|
97
|
+
throw new Error("node-machine-id: machineIdSync not found");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const id = machineIdSync(true);
|
|
101
|
+
return {
|
|
102
|
+
deviceId: id,
|
|
103
|
+
deviceName: os.hostname(),
|
|
104
|
+
env: "node",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 跨平台获取设备信息 */
|
|
109
|
+
export async function getDeviceInfo(): Promise<DeviceInfo> {
|
|
110
|
+
const isBrowser =
|
|
111
|
+
typeof window !== "undefined" &&
|
|
112
|
+
typeof navigator !== "undefined" &&
|
|
113
|
+
typeof screen !== "undefined";
|
|
114
|
+
|
|
115
|
+
return isBrowser ? getBrowserDeviceInfo() : getNodeDeviceInfo();
|
|
116
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
|
|
3
|
+
// 1️⃣ GraphQL client 初始化与获取
|
|
4
|
+
export {
|
|
5
|
+
initGraphQLClient,
|
|
6
|
+
useInterceptor,
|
|
7
|
+
getClient,
|
|
8
|
+
setToken,
|
|
9
|
+
removeToken,
|
|
10
|
+
setEndpoint,
|
|
11
|
+
setHeader,
|
|
12
|
+
setHeaders,
|
|
13
|
+
removeHeader,
|
|
14
|
+
clearHeaders
|
|
15
|
+
} from './core/client.js';
|
|
16
|
+
|
|
17
|
+
// 2️⃣ 类型定义(统一导出)
|
|
18
|
+
export type * from './core/type.js';
|
|
19
|
+
|
|
20
|
+
// 3️⃣ GraphQL 构建器(buildXXX 方法)
|
|
21
|
+
export type {
|
|
22
|
+
//Query
|
|
23
|
+
QueryInput,
|
|
24
|
+
QueryPageListInput,
|
|
25
|
+
QueryByIdInput,
|
|
26
|
+
QueryAggregateInput,
|
|
27
|
+
AggregateFieldInput,
|
|
28
|
+
//Mutation
|
|
29
|
+
InsertOneInput,
|
|
30
|
+
BatchInsertInput,
|
|
31
|
+
DeleteInput,
|
|
32
|
+
DeleteByIdInput,
|
|
33
|
+
BatchUpdateInput,
|
|
34
|
+
UpdateByPkInput,
|
|
35
|
+
UpdateInput,
|
|
36
|
+
//鉴权
|
|
37
|
+
LoginInput,
|
|
38
|
+
RegisterUserInput,
|
|
39
|
+
LogoutInput,
|
|
40
|
+
LogoutDeviceInput,
|
|
41
|
+
RefreshTokenInput,
|
|
42
|
+
//SMS
|
|
43
|
+
SendCodeInput,
|
|
44
|
+
VerifyCodeInput
|
|
45
|
+
} from './builders/index.ts';
|
|
46
|
+
|
|
47
|
+
// 4️⃣ 业务 API 模块(每个模块有默认泛型)
|
|
48
|
+
export * from './core/api/auth.js';
|
|
49
|
+
export * from './core/api/query.js';
|
|
50
|
+
export * from './core/api/mutation.js';
|
|
51
|
+
export * from './core/api/sms.js';
|
|
52
|
+
export * from './core/api/gql.js';
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// 5️⃣ 设备信息
|
|
57
|
+
export * from './device/index.ts'
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
// 6️⃣7️⃣8️⃣9️⃣🔟
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { rateLimitConfig as defaultConfig } from './rateLimitConfig.ts';
|
|
2
|
+
|
|
3
|
+
interface RateLimitRule {
|
|
4
|
+
max: number;
|
|
5
|
+
window: number; // 秒
|
|
6
|
+
debounce: number; // 毫秒
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface TrackerItem {
|
|
10
|
+
timestamps: number[];
|
|
11
|
+
timeout?: NodeJS.Timeout;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const trackers = new Map<string, TrackerItem>();
|
|
15
|
+
|
|
16
|
+
function getRule(operateName: string, type: 'query' | 'mutation', config: any): RateLimitRule {
|
|
17
|
+
return config.custom?.[operateName] || config.defaultRules[type];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rateLimit<T>(
|
|
21
|
+
operateName: string,
|
|
22
|
+
type: 'query' | 'mutation',
|
|
23
|
+
fn: () => Promise<T>,
|
|
24
|
+
config = defaultConfig // <-- 默认使用内置配置
|
|
25
|
+
): Promise<T> {
|
|
26
|
+
const rule = getRule(operateName, type, config);
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const key = `${type}::${operateName}`;
|
|
29
|
+
|
|
30
|
+
if (!trackers.has(key)) trackers.set(key, { timestamps: [] });
|
|
31
|
+
const tracker = trackers.get(key)!;
|
|
32
|
+
|
|
33
|
+
tracker.timestamps = tracker.timestamps.filter(ts => now - ts < rule.window * 1000);
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const exec = () => {
|
|
37
|
+
if (tracker.timestamps.length >= rule.max) {
|
|
38
|
+
return reject(new Error(`Rate limit exceeded for ${operateName}`));
|
|
39
|
+
}
|
|
40
|
+
tracker.timestamps.push(Date.now());
|
|
41
|
+
fn().then(resolve).catch(reject);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (rule.debounce > 0) {
|
|
45
|
+
if (tracker.timeout) clearTimeout(tracker.timeout);
|
|
46
|
+
tracker.timeout = setTimeout(exec, rule.debounce);
|
|
47
|
+
} else {
|
|
48
|
+
exec();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const rateLimitConfig = {
|
|
2
|
+
defaultRules: {
|
|
3
|
+
query: { max: 10, window: 1, debounce: 0 },
|
|
4
|
+
mutation: { max: 3, window: 1, debounce: 200 },
|
|
5
|
+
},
|
|
6
|
+
custom: {
|
|
7
|
+
login: { max: 1, window: 5, debounce: 0 },
|
|
8
|
+
refreshToken: { max: 2, window: 5, debounce: 0 },
|
|
9
|
+
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { initGraphQLClient,auth,query,setToken,removeToken,useInterceptor } from "./index.ts"; // 本地调试用相对路径
|
|
2
|
+
|
|
3
|
+
const client = initGraphQLClient({
|
|
4
|
+
endpoint: "http://localhost:9526/graphql",
|
|
5
|
+
|
|
6
|
+
// Authorization:"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEwMDAwMDAwMDAsImxvZ2luQWNjb3VudCI6ImNhaWNhaSIsInR5cGUiOiJhY2Nlc3MiLCJkZXZpY2VJZCI6IjEwMDAwMDgiLCJqdGkiOiJjMTJjNjQ5ZS05OTIwLTRkNDgtOWRjZS0yYWJkZGMzYTc1OWIiLCJpYXQiOjE3NjE3MzIxMzEsImV4cCI6MTc2MTczMzkzMX0.CkJf3fYdto7zugkkej8sfLCw03Yrgc1zMAoPL2PaFUk"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// ✅ 注册请求 / 响应 / 错误拦截器
|
|
11
|
+
useInterceptor({
|
|
12
|
+
onRequest: async ({ query, variables, headers }) => {
|
|
13
|
+
// console.log("[Request Interceptor]", { query, variables, headers });
|
|
14
|
+
// 可以在这里加上时间戳或日志
|
|
15
|
+
return { query, variables, headers };
|
|
16
|
+
},
|
|
17
|
+
onResponse: async (response) => {
|
|
18
|
+
// console.log("[Response Interceptor]", response);
|
|
19
|
+
// 可以在这里统一处理响应结构,比如包装 data
|
|
20
|
+
return response;
|
|
21
|
+
},
|
|
22
|
+
onError: async (error) => {
|
|
23
|
+
// console.error("[Error Interceptor]", error);
|
|
24
|
+
// 比如 token 过期时可以自动重试或刷新
|
|
25
|
+
if (error.status === 401) {
|
|
26
|
+
console.log("⚠️ Token expired, redirecting to login...");
|
|
27
|
+
}
|
|
28
|
+
// 返回错误或重新抛出
|
|
29
|
+
throw error;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
async function test() {
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// const loginRs=await auth.login({
|
|
37
|
+
// account:"caicai",
|
|
38
|
+
// password:"caicai"
|
|
39
|
+
// })
|
|
40
|
+
// console.log(loginRs.login.token);
|
|
41
|
+
|
|
42
|
+
// setToken(loginRs.login.token);
|
|
43
|
+
// debugger;
|
|
44
|
+
// console.log(loginRs);
|
|
45
|
+
// const rs=await query.byId({
|
|
46
|
+
// operationName:"user",
|
|
47
|
+
// pk:"1000000000",
|
|
48
|
+
// fields:["id","username","nickname","phone",{
|
|
49
|
+
// roles:{
|
|
50
|
+
// fields:["id","roleCode","roleName"]
|
|
51
|
+
// }
|
|
52
|
+
// }]
|
|
53
|
+
// })
|
|
54
|
+
// // debugger;
|
|
55
|
+
// console.log(rs);
|
|
56
|
+
|
|
57
|
+
// removeToken()
|
|
58
|
+
|
|
59
|
+
// const res=await query.byId({
|
|
60
|
+
// operationName:"user",
|
|
61
|
+
// pk:"1000000000",
|
|
62
|
+
// fields:["id","username","nickname","phone",{
|
|
63
|
+
// roles:{
|
|
64
|
+
// fields:["id","roleCode","roleName"]
|
|
65
|
+
// }
|
|
66
|
+
// }]
|
|
67
|
+
// })
|
|
68
|
+
// debugger;
|
|
69
|
+
// console.log(res);
|
|
70
|
+
|
|
71
|
+
} catch (err:any) {
|
|
72
|
+
|
|
73
|
+
// debugger;
|
|
74
|
+
console.error("GraphQL request failed:", err);
|
|
75
|
+
console.error("GraphQL request failed:", err.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
test();
|