@ctil/gql 1.0.5 → 1.0.7
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/dist/index.cjs +15 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +15 -5
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
- package/.vscode/launch.json +0 -18
- package/ctil-gql-1.0.5.tgz +0 -0
- package/src/builders/auth.ts +0 -182
- package/src/builders/baseType.ts +0 -194
- package/src/builders/index.ts +0 -6
- package/src/builders/mutation.ts +0 -341
- package/src/builders/oss.ts +0 -140
- package/src/builders/query.ts +0 -180
- package/src/builders/sms.ts +0 -59
- package/src/cache/memoryCache.ts +0 -34
- package/src/core/api/auth.ts +0 -86
- package/src/core/api/gql.ts +0 -22
- package/src/core/api/mutation.ts +0 -100
- package/src/core/api/oss.ts +0 -216
- package/src/core/api/query.ts +0 -82
- package/src/core/api/sms.ts +0 -18
- package/src/core/client.ts +0 -47
- package/src/core/core.ts +0 -284
- package/src/core/executor.ts +0 -19
- package/src/core/type.ts +0 -76
- package/src/device/index.ts +0 -116
- package/src/index.ts +0 -60
- package/src/rateLimit/rateLimit.ts +0 -51
- package/src/rateLimit/rateLimitConfig.ts +0 -12
- package/src/test.ts +0 -109
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -10
package/src/core/core.ts
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
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 NODE_LOGIN_REMEBER_FILE = path.resolve(process.cwd(), "loginInfo.json.remember");
|
|
16
|
-
const STORAGE_KEY = "GRAPHQL_LOGIN_INFO";
|
|
17
|
-
const REMEMBER_KEY = "GRAPHQL_LOGIN_REMEMBER";
|
|
18
|
-
// Node 内存存储
|
|
19
|
-
let nodeLoginInfo: UserToken | null = null;
|
|
20
|
-
|
|
21
|
-
export class CCRequest {
|
|
22
|
-
private _remember: boolean = false;
|
|
23
|
-
|
|
24
|
-
private client!: GraphQLClient;
|
|
25
|
-
private headers: Record<string, string>;
|
|
26
|
-
private deviceInfoPromise: Promise<{ deviceId: string; deviceName: string }>;
|
|
27
|
-
private interceptors: RequestInterceptor[] = [];
|
|
28
|
-
|
|
29
|
-
constructor(private config: RequestConfig) {
|
|
30
|
-
this.deviceInfoPromise = getDeviceInfo();
|
|
31
|
-
this.headers = this.buildHeaders(config);
|
|
32
|
-
|
|
33
|
-
// 尝试恢复登录信息
|
|
34
|
-
const loginInfo = this.loadLoginInfo();
|
|
35
|
-
if (loginInfo?.token) this.setToken(loginInfo.token);
|
|
36
|
-
|
|
37
|
-
this.buildClient();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 注册一个拦截器(支持多个) */
|
|
41
|
-
use(interceptor: RequestInterceptor) {
|
|
42
|
-
this.interceptors.push(interceptor);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 构建 GraphQLClient 实例 */
|
|
46
|
-
private buildClient() {
|
|
47
|
-
this.client = new GraphQLClient(this.config.endpoint || "/graphql", {
|
|
48
|
-
headers: this.headers,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** 构建初始 headers */
|
|
53
|
-
private buildHeaders(config: RequestConfig): Record<string, string> {
|
|
54
|
-
const headers: Record<string, string> = {
|
|
55
|
-
"Content-Type": "application/json",
|
|
56
|
-
...config.headers,
|
|
57
|
-
};
|
|
58
|
-
if (config.Authorization) {
|
|
59
|
-
headers.Authorization = `Bearer ${config.Authorization}`;
|
|
60
|
-
}
|
|
61
|
-
return headers;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ===== Token 管理 =====
|
|
65
|
-
setToken(token: string) {
|
|
66
|
-
this.config.Authorization = token;
|
|
67
|
-
this.headers.Authorization = `Bearer ${token}`;
|
|
68
|
-
this.buildClient();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
removeToken() {
|
|
72
|
-
this.config.Authorization = undefined;
|
|
73
|
-
delete this.headers.Authorization;
|
|
74
|
-
this.buildClient();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ===== LoginInfo 管理 =====
|
|
78
|
-
setLoginInfo(loginInfo: UserToken, remember: boolean = false) {
|
|
79
|
-
this._remember = remember;
|
|
80
|
-
if (loginInfo.token) this.setToken(loginInfo.token);
|
|
81
|
-
|
|
82
|
-
if (isBrowser) {
|
|
83
|
-
const storage = remember ? localStorage : sessionStorage;
|
|
84
|
-
storage.setItem(STORAGE_KEY, JSON.stringify(loginInfo));
|
|
85
|
-
localStorage.setItem(REMEMBER_KEY, JSON.stringify(remember)); // 持久化 remember
|
|
86
|
-
} else if (isNode) {
|
|
87
|
-
nodeLoginInfo = loginInfo;
|
|
88
|
-
try {
|
|
89
|
-
fs.writeFileSync(NODE_LOGIN_FILE, JSON.stringify(loginInfo), "utf-8");
|
|
90
|
-
fs.writeFileSync(NODE_LOGIN_REMEBER_FILE, JSON.stringify(remember), "utf-8");
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.error("Failed to persist login info to file:", err);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 加载登录信息(仅负责加载,不负责刷新) */
|
|
98
|
-
/** 加载登录信息 */
|
|
99
|
-
loadLoginInfo(): UserToken | null {
|
|
100
|
-
let info: UserToken | null = null;
|
|
101
|
-
|
|
102
|
-
if (isBrowser) {
|
|
103
|
-
const str = localStorage.getItem(STORAGE_KEY) || sessionStorage.getItem(STORAGE_KEY);
|
|
104
|
-
if (str) {
|
|
105
|
-
try {
|
|
106
|
-
info = JSON.parse(str);
|
|
107
|
-
} catch { }
|
|
108
|
-
}
|
|
109
|
-
const rememberStr = localStorage.getItem(REMEMBER_KEY);
|
|
110
|
-
this._remember = rememberStr === "true";
|
|
111
|
-
} else if (isNode) {
|
|
112
|
-
if (nodeLoginInfo) return nodeLoginInfo;
|
|
113
|
-
if (fs.existsSync(NODE_LOGIN_FILE)) {
|
|
114
|
-
try {
|
|
115
|
-
info = JSON.parse(fs.readFileSync(NODE_LOGIN_FILE, "utf-8"));
|
|
116
|
-
|
|
117
|
-
this._remember = fs.existsSync(NODE_LOGIN_REMEBER_FILE)
|
|
118
|
-
? JSON.parse(fs.readFileSync(NODE_LOGIN_REMEBER_FILE, "utf-8"))
|
|
119
|
-
: false;
|
|
120
|
-
} catch { }
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (info) this.config.loginInfo = info;
|
|
125
|
-
return info;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
getLoginInfo(): UserToken | null {
|
|
130
|
-
if (this.config.loginInfo) return this.config.loginInfo;
|
|
131
|
-
return this.loadLoginInfo();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
removeLoginInfo() {
|
|
135
|
-
delete this.config.loginInfo;
|
|
136
|
-
if (isBrowser) {
|
|
137
|
-
localStorage.removeItem(STORAGE_KEY);
|
|
138
|
-
sessionStorage.removeItem(STORAGE_KEY);
|
|
139
|
-
} else if (isNode) {
|
|
140
|
-
nodeLoginInfo = null;
|
|
141
|
-
try {
|
|
142
|
-
if (fs.existsSync(NODE_LOGIN_FILE)) fs.unlinkSync(NODE_LOGIN_FILE);
|
|
143
|
-
if (fs.existsSync(NODE_LOGIN_REMEBER_FILE)) fs.unlinkSync(NODE_LOGIN_REMEBER_FILE);
|
|
144
|
-
} catch { }
|
|
145
|
-
}
|
|
146
|
-
this.removeToken();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ===== Endpoint & Header 管理 =====
|
|
150
|
-
setEndpoint(endpoint: string) {
|
|
151
|
-
this.config.endpoint = endpoint;
|
|
152
|
-
this.buildClient();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
setHeader(key: string, value: string) {
|
|
156
|
-
this.headers[key] = value;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
setHeaders(headers: Record<string, string>) {
|
|
160
|
-
Object.assign(this.headers, headers);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
removeHeader(key: string) {
|
|
164
|
-
delete this.headers[key];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
clearHeaders() {
|
|
168
|
-
this.headers = { "Content-Type": "application/json" };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//无感刷新
|
|
173
|
-
/** 无感刷新 token */
|
|
174
|
-
private async ensureTokenValid(): Promise<void> {
|
|
175
|
-
const loginInfo = this.getLoginInfo();
|
|
176
|
-
if (!loginInfo) return;
|
|
177
|
-
|
|
178
|
-
const now = Date.now();
|
|
179
|
-
const accessExpired = new Date(loginInfo.expireAt).getTime() <= now;
|
|
180
|
-
const refreshExpired = new Date(loginInfo.refreshExpireAt).getTime() <= now;
|
|
181
|
-
|
|
182
|
-
if (refreshExpired) {
|
|
183
|
-
this.removeLoginInfo();
|
|
184
|
-
throw new Error("Login expired. Please login again.");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (accessExpired && !refreshExpired) {
|
|
188
|
-
try {
|
|
189
|
-
const refreshResult = await auth.refreshToken({
|
|
190
|
-
refreshToken: loginInfo.refreshToken,
|
|
191
|
-
remember: this._remember, // 使用持久化的 remember
|
|
192
|
-
});
|
|
193
|
-
const newInfo: UserToken = (refreshResult as any).refreshToken ?? refreshResult;
|
|
194
|
-
this.setLoginInfo(newInfo, this._remember);
|
|
195
|
-
} catch {
|
|
196
|
-
this.removeLoginInfo();
|
|
197
|
-
throw new Error("Failed to refresh token. Please login again.");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// ===== 请求逻辑 =====
|
|
202
|
-
async request<T = any>(
|
|
203
|
-
query: string | DocumentNode,
|
|
204
|
-
variables?: Record<string, any>
|
|
205
|
-
): Promise<T> {
|
|
206
|
-
let queryStr = typeof query === "string" ? query : print(query);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// 🚫 如果是 refreshToken 请求,跳过 token 校验,避免死循环
|
|
210
|
-
if (!/refreshToken/i.test(queryStr)) {
|
|
211
|
-
await this.ensureTokenValid();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const { deviceId, deviceName } = await this.deviceInfoPromise;
|
|
216
|
-
|
|
217
|
-
let headersWithDevice: Record<string, string> = Object.fromEntries(
|
|
218
|
-
Object.entries({
|
|
219
|
-
...this.headers,
|
|
220
|
-
"X-Device-Id": deviceId,
|
|
221
|
-
"X-Device-Name": deviceName,
|
|
222
|
-
}).filter(([_, v]) => v !== undefined)
|
|
223
|
-
) as Record<string, string>;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// === 执行所有 onRequest 拦截器 ===
|
|
228
|
-
for (const interceptor of this.interceptors) {
|
|
229
|
-
if (interceptor.onRequest) {
|
|
230
|
-
const result = await interceptor.onRequest({
|
|
231
|
-
query: queryStr,
|
|
232
|
-
variables,
|
|
233
|
-
headers: headersWithDevice,
|
|
234
|
-
});
|
|
235
|
-
queryStr = result.query;
|
|
236
|
-
variables = result.variables;
|
|
237
|
-
headersWithDevice = Object.fromEntries(
|
|
238
|
-
Object.entries(result.headers).filter(([_, v]) => v !== undefined)
|
|
239
|
-
) as Record<string, string>;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const res = await this.client.rawRequest<T>(
|
|
245
|
-
queryStr,
|
|
246
|
-
variables,
|
|
247
|
-
headersWithDevice
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
let data = res.data;
|
|
251
|
-
|
|
252
|
-
// === 执行所有 onResponse 拦截器 ===
|
|
253
|
-
for (const interceptor of this.interceptors) {
|
|
254
|
-
if (interceptor.onResponse) {
|
|
255
|
-
data = await interceptor.onResponse(data);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return data;
|
|
260
|
-
} catch (err: any) {
|
|
261
|
-
const message = err.response?.errors?.[0]?.message ?? err.message;
|
|
262
|
-
const status = err.response?.errors?.[0]?.extensions?.code ?? 500;
|
|
263
|
-
|
|
264
|
-
const formattedError = {
|
|
265
|
-
message,
|
|
266
|
-
status,
|
|
267
|
-
data: err.response?.data ?? null,
|
|
268
|
-
errors: err.response?.errors ?? null,
|
|
269
|
-
request: err.request,
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
// === 执行所有 onError 拦截器 ===
|
|
273
|
-
for (const interceptor of this.interceptors) {
|
|
274
|
-
if (interceptor.onError) {
|
|
275
|
-
await interceptor.onError(formattedError);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
throw formattedError;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
package/src/core/executor.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
|
package/src/device/index.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
export * from './core/api/oss.js';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// 5️⃣ 设备信息
|
|
57
|
-
export * from './device/index.ts'
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// 6️⃣7️⃣8️⃣9️⃣🔟
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
|