apaas-oapi-client 0.1.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 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.js +293 -0
- package/dist/limiter.d.ts +17 -0
- package/dist/logger.d.ts +11 -0
- package/package.json +37 -0
- package/src/index.ts +382 -0
- package/src/limiter.ts +29 -0
- package/src/logger.ts +11 -0
package/README.md
ADDED
|
File without changes
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { LoggerLevel } from './logger';
|
|
2
|
+
/**
|
|
3
|
+
* Client 初始化配置
|
|
4
|
+
*/
|
|
5
|
+
interface ClientOptions {
|
|
6
|
+
/** 命名空间,例如 app_xxx */
|
|
7
|
+
namespace: string;
|
|
8
|
+
/** 应用 clientId */
|
|
9
|
+
clientId: string;
|
|
10
|
+
/** 应用 clientSecret */
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
/** 是否禁用 token 缓存,每次调用强制刷新 token,默认 false */
|
|
13
|
+
disableTokenCache?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* records_query 接口请求参数
|
|
17
|
+
*/
|
|
18
|
+
interface RecordsQueryParams {
|
|
19
|
+
/** 对象名称,例如 object_store */
|
|
20
|
+
object_name: string;
|
|
21
|
+
/** 请求体数据 */
|
|
22
|
+
data: any;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* aPaaS OpenAPI 客户端
|
|
26
|
+
*/
|
|
27
|
+
declare class Client {
|
|
28
|
+
private clientId;
|
|
29
|
+
private clientSecret;
|
|
30
|
+
private namespace;
|
|
31
|
+
private disableTokenCache;
|
|
32
|
+
private accessToken;
|
|
33
|
+
private expireTime;
|
|
34
|
+
private axiosInstance;
|
|
35
|
+
private loggerLevel;
|
|
36
|
+
/**
|
|
37
|
+
* 构造函数
|
|
38
|
+
* @param options ClientOptions
|
|
39
|
+
*/
|
|
40
|
+
constructor(options: ClientOptions);
|
|
41
|
+
/**
|
|
42
|
+
* 设置日志等级
|
|
43
|
+
* @param level LoggerLevel
|
|
44
|
+
*/
|
|
45
|
+
setLoggerLevel(level: LoggerLevel): void;
|
|
46
|
+
/**
|
|
47
|
+
* 日志打印方法
|
|
48
|
+
* @param level LoggerLevel
|
|
49
|
+
* @param args 打印内容
|
|
50
|
+
*/
|
|
51
|
+
private log;
|
|
52
|
+
/**
|
|
53
|
+
* 初始化 client,自动获取 token
|
|
54
|
+
*/
|
|
55
|
+
init(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* 获取 accessToken
|
|
58
|
+
*/
|
|
59
|
+
private getAccessToken;
|
|
60
|
+
/**
|
|
61
|
+
* 确保 token 有效,若过期则刷新
|
|
62
|
+
*/
|
|
63
|
+
private ensureTokenValid;
|
|
64
|
+
/**
|
|
65
|
+
* 获取当前 accessToken
|
|
66
|
+
*/
|
|
67
|
+
get token(): string | null;
|
|
68
|
+
/**
|
|
69
|
+
* 获取当前 namespace
|
|
70
|
+
*/
|
|
71
|
+
get currentNamespace(): string;
|
|
72
|
+
/**
|
|
73
|
+
* 对象模块
|
|
74
|
+
*/
|
|
75
|
+
object: {
|
|
76
|
+
search: {
|
|
77
|
+
/**
|
|
78
|
+
* 单条记录查询
|
|
79
|
+
* @param params 请求参数
|
|
80
|
+
* @returns 接口返回结果
|
|
81
|
+
*/
|
|
82
|
+
record: (params: {
|
|
83
|
+
object_name: string;
|
|
84
|
+
record_id: string;
|
|
85
|
+
select: string[];
|
|
86
|
+
}) => Promise<any>;
|
|
87
|
+
/**
|
|
88
|
+
* records_query 接口
|
|
89
|
+
* @param params 请求参数
|
|
90
|
+
* @returns 接口返回结果
|
|
91
|
+
*/
|
|
92
|
+
records: (params: RecordsQueryParams) => Promise<any>;
|
|
93
|
+
/**
|
|
94
|
+
* 分页查询所有记录
|
|
95
|
+
* @param params 请求参数
|
|
96
|
+
* @returns { total, items }
|
|
97
|
+
*/
|
|
98
|
+
recordsWithIterator: (params: RecordsQueryParams) => Promise<{
|
|
99
|
+
total: number;
|
|
100
|
+
items: any[];
|
|
101
|
+
}>;
|
|
102
|
+
};
|
|
103
|
+
update: {
|
|
104
|
+
/**
|
|
105
|
+
* 单条更新
|
|
106
|
+
* @param params 请求参数
|
|
107
|
+
* @returns 接口返回结果
|
|
108
|
+
*/
|
|
109
|
+
record: (params: {
|
|
110
|
+
object_name: string;
|
|
111
|
+
record_id: string;
|
|
112
|
+
record: any;
|
|
113
|
+
}) => Promise<any>;
|
|
114
|
+
/**
|
|
115
|
+
* 批量更新
|
|
116
|
+
* @param params 请求参数
|
|
117
|
+
* @returns 所有子请求的返回结果数组
|
|
118
|
+
*/
|
|
119
|
+
recordsBatchUpdate: (params: {
|
|
120
|
+
object_name: string;
|
|
121
|
+
records: any[];
|
|
122
|
+
}) => Promise<any[]>;
|
|
123
|
+
};
|
|
124
|
+
delete: {
|
|
125
|
+
/**
|
|
126
|
+
* 单条删除
|
|
127
|
+
* @param params 请求参数
|
|
128
|
+
* @returns 接口返回结果
|
|
129
|
+
*/
|
|
130
|
+
record: (params: {
|
|
131
|
+
object_name: string;
|
|
132
|
+
record_id: string;
|
|
133
|
+
}) => Promise<any>;
|
|
134
|
+
/**
|
|
135
|
+
* 批量删除
|
|
136
|
+
* @param params 请求参数
|
|
137
|
+
* @returns 所有子请求的返回结果数组
|
|
138
|
+
*/
|
|
139
|
+
recordsBatchDelete: (params: {
|
|
140
|
+
object_name: string;
|
|
141
|
+
ids: string[];
|
|
142
|
+
}) => Promise<any[]>;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export declare const apaas: {
|
|
147
|
+
Client: typeof Client;
|
|
148
|
+
};
|
|
149
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var dayjs = require('dayjs');
|
|
4
|
+
var axios = require('axios');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 日志等级枚举
|
|
8
|
+
*/
|
|
9
|
+
var LoggerLevel;
|
|
10
|
+
(function (LoggerLevel) {
|
|
11
|
+
LoggerLevel[LoggerLevel["fatal"] = 0] = "fatal";
|
|
12
|
+
LoggerLevel[LoggerLevel["error"] = 1] = "error";
|
|
13
|
+
LoggerLevel[LoggerLevel["warn"] = 2] = "warn";
|
|
14
|
+
LoggerLevel[LoggerLevel["info"] = 3] = "info";
|
|
15
|
+
LoggerLevel[LoggerLevel["debug"] = 4] = "debug";
|
|
16
|
+
LoggerLevel[LoggerLevel["trace"] = 5] = "trace";
|
|
17
|
+
})(LoggerLevel || (LoggerLevel = {}));
|
|
18
|
+
|
|
19
|
+
const { functionLimiter } = require('./limiter');
|
|
20
|
+
/**
|
|
21
|
+
* aPaaS OpenAPI 客户端
|
|
22
|
+
*/
|
|
23
|
+
class Client {
|
|
24
|
+
/**
|
|
25
|
+
* 构造函数
|
|
26
|
+
* @param options ClientOptions
|
|
27
|
+
*/
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.accessToken = null;
|
|
30
|
+
this.expireTime = null;
|
|
31
|
+
this.loggerLevel = LoggerLevel.info;
|
|
32
|
+
/**
|
|
33
|
+
* 对象模块
|
|
34
|
+
*/
|
|
35
|
+
this.object = {
|
|
36
|
+
search: {
|
|
37
|
+
/**
|
|
38
|
+
* 单条记录查询
|
|
39
|
+
* @param params 请求参数
|
|
40
|
+
* @returns 接口返回结果
|
|
41
|
+
*/
|
|
42
|
+
record: async (params) => {
|
|
43
|
+
const { object_name, record_id, select } = params;
|
|
44
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records/${record_id}`;
|
|
45
|
+
this.log(LoggerLevel.info, `[单条查询记录] 🔍 开始查询 record_id: ${record_id}`);
|
|
46
|
+
const res = await functionLimiter(async () => {
|
|
47
|
+
await this.ensureTokenValid();
|
|
48
|
+
const response = await this.axiosInstance.post(url, { select }, { headers: { Authorization: `${this.accessToken}` } });
|
|
49
|
+
this.log(LoggerLevel.info, `[单条查询记录] 🔍 record_id: ${record_id} 查询完成,返回 code: ${response.data.code}`);
|
|
50
|
+
return response.data;
|
|
51
|
+
});
|
|
52
|
+
return res;
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* records_query 接口
|
|
56
|
+
* @param params 请求参数
|
|
57
|
+
* @returns 接口返回结果
|
|
58
|
+
*/
|
|
59
|
+
records: async (params) => {
|
|
60
|
+
const { object_name, data } = params;
|
|
61
|
+
await this.ensureTokenValid();
|
|
62
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records_query`;
|
|
63
|
+
const res = await this.axiosInstance.post(url, data, {
|
|
64
|
+
headers: { Authorization: `${this.accessToken}` }
|
|
65
|
+
});
|
|
66
|
+
this.log(LoggerLevel.debug, `[批量查询记录] 🔍 records_query 调用完成,object_name: ${object_name}`);
|
|
67
|
+
return res.data;
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* 分页查询所有记录
|
|
71
|
+
* @param params 请求参数
|
|
72
|
+
* @returns { total, items }
|
|
73
|
+
*/
|
|
74
|
+
recordsWithIterator: async (params) => {
|
|
75
|
+
const { object_name, data } = params;
|
|
76
|
+
let results = [];
|
|
77
|
+
let nextPageToken = '';
|
|
78
|
+
let total = 0;
|
|
79
|
+
let page = 0;
|
|
80
|
+
do {
|
|
81
|
+
await functionLimiter(async () => {
|
|
82
|
+
const mergedData = { ...data, page_token: nextPageToken || '' };
|
|
83
|
+
const res = await this.object.search.records({
|
|
84
|
+
object_name,
|
|
85
|
+
data: mergedData
|
|
86
|
+
});
|
|
87
|
+
page += 1;
|
|
88
|
+
if (res.data && Array.isArray(res.data.items)) {
|
|
89
|
+
results = results.concat(res.data.items);
|
|
90
|
+
}
|
|
91
|
+
if (page === 1) {
|
|
92
|
+
total = res.data.total || 0;
|
|
93
|
+
this.log(LoggerLevel.info, '[批量查询记录] 🔍 接口返回 total:', total);
|
|
94
|
+
}
|
|
95
|
+
nextPageToken = res.data.next_page_token;
|
|
96
|
+
this.log(LoggerLevel.debug, `[批量查询记录] 🔍 第 ${page} 页查询完成,items.length: ${res.data.items.length}`);
|
|
97
|
+
return res;
|
|
98
|
+
});
|
|
99
|
+
} while (nextPageToken);
|
|
100
|
+
return { total, items: results };
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
update: {
|
|
104
|
+
/**
|
|
105
|
+
* 单条更新
|
|
106
|
+
* @param params 请求参数
|
|
107
|
+
* @returns 接口返回结果
|
|
108
|
+
*/
|
|
109
|
+
record: async (params) => {
|
|
110
|
+
const { object_name, record_id, record } = params;
|
|
111
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records/${record_id}`;
|
|
112
|
+
this.log(LoggerLevel.info, `[单条更新记录] 💾 开始更新 record_id: ${record_id}`);
|
|
113
|
+
const res = await functionLimiter(async () => {
|
|
114
|
+
await this.ensureTokenValid();
|
|
115
|
+
const response = await this.axiosInstance.patch(url, { record }, { headers: { Authorization: `${this.accessToken}` } });
|
|
116
|
+
this.log(LoggerLevel.info, `[单条更新记录] 💾 record_id: ${record_id} 更新完成,返回 code: ${response.data.code}`);
|
|
117
|
+
return response.data;
|
|
118
|
+
});
|
|
119
|
+
return res;
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* 批量更新
|
|
123
|
+
* @param params 请求参数
|
|
124
|
+
* @returns 所有子请求的返回结果数组
|
|
125
|
+
*/
|
|
126
|
+
recordsBatchUpdate: async (params) => {
|
|
127
|
+
const { object_name, records } = params;
|
|
128
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records_batch`;
|
|
129
|
+
const chunkSize = 100;
|
|
130
|
+
const chunks = [];
|
|
131
|
+
for (let i = 0; i < records.length; i += chunkSize) {
|
|
132
|
+
chunks.push(records.slice(i, i + chunkSize));
|
|
133
|
+
}
|
|
134
|
+
this.log(LoggerLevel.info, `[批量更新记录] 💾 总共 ${records.length} 条记录,拆分为 ${chunks.length} 组,每组最多 ${chunkSize} 条`);
|
|
135
|
+
const results = [];
|
|
136
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
137
|
+
this.log(LoggerLevel.info, `[批量更新记录] 💾 开始更新第 ${index + 1} 组,共 ${chunk.length} 条`);
|
|
138
|
+
const res = await functionLimiter(async () => {
|
|
139
|
+
await this.ensureTokenValid();
|
|
140
|
+
const response = await this.axiosInstance.patch(url, { records: chunk }, { headers: { Authorization: `${this.accessToken}` } });
|
|
141
|
+
this.log(LoggerLevel.info, `[批量更新记录] 💾 第 ${index + 1} 组更新完成,返回 code: ${response.data.code}`);
|
|
142
|
+
return response.data;
|
|
143
|
+
});
|
|
144
|
+
results.push(res);
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
delete: {
|
|
150
|
+
/**
|
|
151
|
+
* 单条删除
|
|
152
|
+
* @param params 请求参数
|
|
153
|
+
* @returns 接口返回结果
|
|
154
|
+
*/
|
|
155
|
+
record: async (params) => {
|
|
156
|
+
const { object_name, record_id } = params;
|
|
157
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records/${record_id}`;
|
|
158
|
+
this.log(LoggerLevel.info, `[单条删除记录] 🗑️ 开始删除 record_id: ${record_id}`);
|
|
159
|
+
const res = await functionLimiter(async () => {
|
|
160
|
+
await this.ensureTokenValid();
|
|
161
|
+
const response = await this.axiosInstance.delete(url, {
|
|
162
|
+
headers: { Authorization: `${this.accessToken}` }
|
|
163
|
+
});
|
|
164
|
+
this.log(LoggerLevel.info, `[单条删除记录] 🗑️ record_id: ${record_id} 删除完成,返回 code: ${response.data.code}`);
|
|
165
|
+
return response.data;
|
|
166
|
+
});
|
|
167
|
+
return res;
|
|
168
|
+
},
|
|
169
|
+
/**
|
|
170
|
+
* 批量删除
|
|
171
|
+
* @param params 请求参数
|
|
172
|
+
* @returns 所有子请求的返回结果数组
|
|
173
|
+
*/
|
|
174
|
+
recordsBatchDelete: async (params) => {
|
|
175
|
+
const { object_name, ids } = params;
|
|
176
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records_batch`;
|
|
177
|
+
const chunkSize = 100;
|
|
178
|
+
const chunks = [];
|
|
179
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
180
|
+
chunks.push(ids.slice(i, i + chunkSize));
|
|
181
|
+
}
|
|
182
|
+
this.log(LoggerLevel.info, `[批量删除记录] 🗑️ 总共 ${ids.length} 条记录,拆分为 ${chunks.length} 组,每组最多 ${chunkSize} 条`);
|
|
183
|
+
const results = [];
|
|
184
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
185
|
+
this.log(LoggerLevel.info, `[批量删除记录] 🗑️ 开始删除第 ${index + 1} 组,共 ${chunk.length} 条`);
|
|
186
|
+
const res = await functionLimiter(async () => {
|
|
187
|
+
await this.ensureTokenValid();
|
|
188
|
+
const response = await this.axiosInstance.delete(url, {
|
|
189
|
+
headers: { Authorization: `${this.accessToken}` },
|
|
190
|
+
data: { ids: chunk }
|
|
191
|
+
});
|
|
192
|
+
this.log(LoggerLevel.info, `[批量删除记录] 🗑️ 第 ${index + 1} 组删除完成,返回 code: ${response.data.code}`);
|
|
193
|
+
return response.data;
|
|
194
|
+
});
|
|
195
|
+
results.push(res);
|
|
196
|
+
}
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
this.clientId = options.clientId;
|
|
202
|
+
this.clientSecret = options.clientSecret;
|
|
203
|
+
this.namespace = options.namespace;
|
|
204
|
+
this.disableTokenCache = options.disableTokenCache || false;
|
|
205
|
+
this.axiosInstance = axios.create({
|
|
206
|
+
baseURL: 'https://ae-openapi.feishu.cn',
|
|
207
|
+
headers: { 'Content-Type': 'application/json' }
|
|
208
|
+
});
|
|
209
|
+
this.log(LoggerLevel.info, 'client initialized');
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 设置日志等级
|
|
213
|
+
* @param level LoggerLevel
|
|
214
|
+
*/
|
|
215
|
+
setLoggerLevel(level) {
|
|
216
|
+
this.loggerLevel = level;
|
|
217
|
+
this.log(LoggerLevel.info, `logger level set to ${LoggerLevel[level]}`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 日志打印方法
|
|
221
|
+
* @param level LoggerLevel
|
|
222
|
+
* @param args 打印内容
|
|
223
|
+
*/
|
|
224
|
+
log(level, ...args) {
|
|
225
|
+
if (this.loggerLevel >= level) {
|
|
226
|
+
const levelStr = LoggerLevel[level];
|
|
227
|
+
const timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss:SSS');
|
|
228
|
+
console.log(`[${levelStr}] [${timestamp}]`, ...args);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 初始化 client,自动获取 token
|
|
233
|
+
*/
|
|
234
|
+
async init() {
|
|
235
|
+
await this.ensureTokenValid();
|
|
236
|
+
this.log(LoggerLevel.info, 'client ready');
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 获取 accessToken
|
|
240
|
+
*/
|
|
241
|
+
async getAccessToken() {
|
|
242
|
+
const url = '/auth/v1/appToken';
|
|
243
|
+
const res = await this.axiosInstance.post(url, {
|
|
244
|
+
clientId: this.clientId,
|
|
245
|
+
clientSecret: this.clientSecret
|
|
246
|
+
});
|
|
247
|
+
if (res.data.code !== '0') {
|
|
248
|
+
this.log(LoggerLevel.error, `[获取认证] 获取 accessToken 失败: ${res.data.msg}`);
|
|
249
|
+
throw new Error(`获取 accessToken 失败: ${res.data.msg}`);
|
|
250
|
+
}
|
|
251
|
+
this.accessToken = res.data.data.accessToken;
|
|
252
|
+
this.expireTime = res.data.data.expireTime;
|
|
253
|
+
this.log(LoggerLevel.info, '[获取认证] accessToken refreshed');
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 确保 token 有效,若过期则刷新
|
|
257
|
+
*/
|
|
258
|
+
async ensureTokenValid() {
|
|
259
|
+
if (this.disableTokenCache) {
|
|
260
|
+
this.log(LoggerLevel.debug, '[获取认证] token cache disabled, refreshing token');
|
|
261
|
+
await this.getAccessToken();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!this.accessToken || !this.expireTime) {
|
|
265
|
+
this.log(LoggerLevel.debug, '[获取认证] no token cached, fetching new token');
|
|
266
|
+
await this.getAccessToken();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const now = dayjs().valueOf();
|
|
270
|
+
if (now + 60 * 1000 > this.expireTime) {
|
|
271
|
+
this.log(LoggerLevel.debug, '[获取认证] token expired, refreshing');
|
|
272
|
+
await this.getAccessToken();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 获取当前 accessToken
|
|
277
|
+
*/
|
|
278
|
+
get token() {
|
|
279
|
+
return this.accessToken;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 获取当前 namespace
|
|
283
|
+
*/
|
|
284
|
+
get currentNamespace() {
|
|
285
|
+
this.log(LoggerLevel.debug, `[获取命名空间] 当前命名空间: ${this.namespace}`);
|
|
286
|
+
return this.namespace;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const apaas = {
|
|
290
|
+
Client
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
exports.apaas = apaas;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Bottleneck from 'bottleneck';
|
|
2
|
+
/**
|
|
3
|
+
* 默认 apaas 限流配置
|
|
4
|
+
*/
|
|
5
|
+
export declare const apaasLimiterOptions: {
|
|
6
|
+
minTime: number;
|
|
7
|
+
reservoir: number;
|
|
8
|
+
reservoirRefreshAmount: number;
|
|
9
|
+
reservoirRefreshInterval: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* 创建限流器
|
|
13
|
+
* @param fn 被限流函数
|
|
14
|
+
* @param options 自定义限流配置
|
|
15
|
+
* @returns 包装后的限流函数
|
|
16
|
+
*/
|
|
17
|
+
export declare function functionLimiter<T>(fn: () => Promise<T>, options?: Partial<Bottleneck.ConstructorOptions>): Promise<T>;
|
package/dist/logger.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apaas-oapi-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"exports": {
|
|
5
|
+
"./node-sdk": "./dist/index.js"
|
|
6
|
+
},
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "rollup -c",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"dev": "ts-node src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"axios": "^1.10.0",
|
|
20
|
+
"bottleneck": "^2.19.5",
|
|
21
|
+
"dayjs": "^1.11.13"
|
|
22
|
+
},
|
|
23
|
+
"directories": {
|
|
24
|
+
"test": "test"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/jest": "^30.0.0",
|
|
28
|
+
"@types/node": "^24.0.7",
|
|
29
|
+
"jest": "^30.0.3",
|
|
30
|
+
"rollup": "^4.44.1",
|
|
31
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
32
|
+
"ts-jest": "^29.4.0",
|
|
33
|
+
"ts-node": "^10.9.2",
|
|
34
|
+
"typescript": "^5.8.3"
|
|
35
|
+
},
|
|
36
|
+
"description": ""
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import axios, { AxiosInstance } from 'axios';
|
|
3
|
+
import { LoggerLevel } from './logger';
|
|
4
|
+
const { functionLimiter } = require('./limiter');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client 初始化配置
|
|
8
|
+
*/
|
|
9
|
+
interface ClientOptions {
|
|
10
|
+
/** 命名空间,例如 app_xxx */
|
|
11
|
+
namespace: string;
|
|
12
|
+
/** 应用 clientId */
|
|
13
|
+
clientId: string;
|
|
14
|
+
/** 应用 clientSecret */
|
|
15
|
+
clientSecret: string;
|
|
16
|
+
/** 是否禁用 token 缓存,每次调用强制刷新 token,默认 false */
|
|
17
|
+
disableTokenCache?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 获取 token 接口返回体
|
|
22
|
+
*/
|
|
23
|
+
interface TokenResponse {
|
|
24
|
+
code: string;
|
|
25
|
+
data: {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
expireTime: number; // 过期时间戳
|
|
28
|
+
};
|
|
29
|
+
msg: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* records_query 接口请求参数
|
|
34
|
+
*/
|
|
35
|
+
interface RecordsQueryParams {
|
|
36
|
+
/** 对象名称,例如 object_store */
|
|
37
|
+
object_name: string;
|
|
38
|
+
/** 请求体数据 */
|
|
39
|
+
data: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* aPaaS OpenAPI 客户端
|
|
44
|
+
*/
|
|
45
|
+
class Client {
|
|
46
|
+
private clientId: string;
|
|
47
|
+
private clientSecret: string;
|
|
48
|
+
private namespace: string;
|
|
49
|
+
private disableTokenCache: boolean;
|
|
50
|
+
private accessToken: string | null = null;
|
|
51
|
+
private expireTime: number | null = null;
|
|
52
|
+
private axiosInstance: AxiosInstance;
|
|
53
|
+
private loggerLevel: LoggerLevel = LoggerLevel.info;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 构造函数
|
|
57
|
+
* @param options ClientOptions
|
|
58
|
+
*/
|
|
59
|
+
constructor(options: ClientOptions) {
|
|
60
|
+
this.clientId = options.clientId;
|
|
61
|
+
this.clientSecret = options.clientSecret;
|
|
62
|
+
this.namespace = options.namespace;
|
|
63
|
+
this.disableTokenCache = options.disableTokenCache || false;
|
|
64
|
+
|
|
65
|
+
this.axiosInstance = axios.create({
|
|
66
|
+
baseURL: 'https://ae-openapi.feishu.cn',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' }
|
|
68
|
+
});
|
|
69
|
+
this.log(LoggerLevel.info, 'client initialized');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 设置日志等级
|
|
74
|
+
* @param level LoggerLevel
|
|
75
|
+
*/
|
|
76
|
+
setLoggerLevel(level: LoggerLevel) {
|
|
77
|
+
this.loggerLevel = level;
|
|
78
|
+
this.log(LoggerLevel.info, `logger level set to ${LoggerLevel[level]}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 日志打印方法
|
|
83
|
+
* @param level LoggerLevel
|
|
84
|
+
* @param args 打印内容
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
private log(level: LoggerLevel, ...args: any[]) {
|
|
88
|
+
if (this.loggerLevel >= level) {
|
|
89
|
+
const levelStr = LoggerLevel[level];
|
|
90
|
+
const timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss:SSS');
|
|
91
|
+
console.log(`[${levelStr}] [${timestamp}]`, ...args);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 初始化 client,自动获取 token
|
|
96
|
+
*/
|
|
97
|
+
async init() {
|
|
98
|
+
await this.ensureTokenValid();
|
|
99
|
+
this.log(LoggerLevel.info, 'client ready');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 获取 accessToken
|
|
104
|
+
*/
|
|
105
|
+
private async getAccessToken(): Promise<void> {
|
|
106
|
+
const url = '/auth/v1/appToken';
|
|
107
|
+
const res = await this.axiosInstance.post<TokenResponse>(url, {
|
|
108
|
+
clientId: this.clientId,
|
|
109
|
+
clientSecret: this.clientSecret
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (res.data.code !== '0') {
|
|
113
|
+
this.log(LoggerLevel.error, `[获取认证] 获取 accessToken 失败: ${res.data.msg}`);
|
|
114
|
+
throw new Error(`获取 accessToken 失败: ${res.data.msg}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.accessToken = res.data.data.accessToken;
|
|
118
|
+
this.expireTime = res.data.data.expireTime;
|
|
119
|
+
this.log(LoggerLevel.info, '[获取认证] accessToken refreshed');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 确保 token 有效,若过期则刷新
|
|
124
|
+
*/
|
|
125
|
+
private async ensureTokenValid() {
|
|
126
|
+
if (this.disableTokenCache) {
|
|
127
|
+
this.log(LoggerLevel.debug, '[获取认证] token cache disabled, refreshing token');
|
|
128
|
+
await this.getAccessToken();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!this.accessToken || !this.expireTime) {
|
|
133
|
+
this.log(LoggerLevel.debug, '[获取认证] no token cached, fetching new token');
|
|
134
|
+
await this.getAccessToken();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const now = dayjs().valueOf();
|
|
139
|
+
if (now + 60 * 1000 > this.expireTime) {
|
|
140
|
+
this.log(LoggerLevel.debug, '[获取认证] token expired, refreshing');
|
|
141
|
+
await this.getAccessToken();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 获取当前 accessToken
|
|
147
|
+
*/
|
|
148
|
+
get token() {
|
|
149
|
+
return this.accessToken;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 获取当前 namespace
|
|
154
|
+
*/
|
|
155
|
+
get currentNamespace() {
|
|
156
|
+
this.log(LoggerLevel.debug, `[获取命名空间] 当前命名空间: ${this.namespace}`);
|
|
157
|
+
return this.namespace;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 对象模块
|
|
162
|
+
*/
|
|
163
|
+
public object = {
|
|
164
|
+
search: {
|
|
165
|
+
/**
|
|
166
|
+
* 单条记录查询
|
|
167
|
+
* @param params 请求参数
|
|
168
|
+
* @returns 接口返回结果
|
|
169
|
+
*/
|
|
170
|
+
record: async (params: { object_name: string; record_id: string; select: string[] }): Promise<any> => {
|
|
171
|
+
const { object_name, record_id, select } = params;
|
|
172
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records/${record_id}`;
|
|
173
|
+
|
|
174
|
+
this.log(LoggerLevel.info, `[单条查询记录] 🔍 开始查询 record_id: ${record_id}`);
|
|
175
|
+
|
|
176
|
+
const res = await functionLimiter(async () => {
|
|
177
|
+
await this.ensureTokenValid();
|
|
178
|
+
|
|
179
|
+
const response = await this.axiosInstance.post(url, { select }, { headers: { Authorization: `${this.accessToken}` } });
|
|
180
|
+
|
|
181
|
+
this.log(LoggerLevel.info, `[单条查询记录] 🔍 record_id: ${record_id} 查询完成,返回 code: ${response.data.code}`);
|
|
182
|
+
return response.data;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return res;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* records_query 接口
|
|
190
|
+
* @param params 请求参数
|
|
191
|
+
* @returns 接口返回结果
|
|
192
|
+
*/
|
|
193
|
+
records: async (params: RecordsQueryParams): Promise<any> => {
|
|
194
|
+
const { object_name, data } = params;
|
|
195
|
+
await this.ensureTokenValid();
|
|
196
|
+
|
|
197
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records_query`;
|
|
198
|
+
|
|
199
|
+
const res = await this.axiosInstance.post(url, data, {
|
|
200
|
+
headers: { Authorization: `${this.accessToken}` }
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
this.log(LoggerLevel.debug, `[批量查询记录] 🔍 records_query 调用完成,object_name: ${object_name}`);
|
|
204
|
+
return res.data;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 分页查询所有记录
|
|
209
|
+
* @param params 请求参数
|
|
210
|
+
* @returns { total, items }
|
|
211
|
+
*/
|
|
212
|
+
recordsWithIterator: async (params: RecordsQueryParams): Promise<{ total: number; items: any[] }> => {
|
|
213
|
+
const { object_name, data } = params;
|
|
214
|
+
|
|
215
|
+
let results: any[] = [];
|
|
216
|
+
let nextPageToken: string | undefined = '';
|
|
217
|
+
let total = 0;
|
|
218
|
+
let page = 0;
|
|
219
|
+
|
|
220
|
+
do {
|
|
221
|
+
const pageRes = await functionLimiter(async () => {
|
|
222
|
+
const mergedData = { ...data, page_token: nextPageToken || '' };
|
|
223
|
+
|
|
224
|
+
const res = await this.object.search.records({
|
|
225
|
+
object_name,
|
|
226
|
+
data: mergedData
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
page += 1;
|
|
230
|
+
|
|
231
|
+
if (res.data && Array.isArray(res.data.items)) {
|
|
232
|
+
results = results.concat(res.data.items);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (page === 1) {
|
|
236
|
+
total = res.data.total || 0;
|
|
237
|
+
this.log(LoggerLevel.info, '[批量查询记录] 🔍 接口返回 total:', total);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
nextPageToken = res.data.next_page_token;
|
|
241
|
+
|
|
242
|
+
this.log(LoggerLevel.debug, `[批量查询记录] 🔍 第 ${page} 页查询完成,items.length: ${res.data.items.length}`);
|
|
243
|
+
return res;
|
|
244
|
+
});
|
|
245
|
+
} while (nextPageToken);
|
|
246
|
+
|
|
247
|
+
return { total, items: results };
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
update: {
|
|
252
|
+
/**
|
|
253
|
+
* 单条更新
|
|
254
|
+
* @param params 请求参数
|
|
255
|
+
* @returns 接口返回结果
|
|
256
|
+
*/
|
|
257
|
+
record: async (params: { object_name: string; record_id: string; record: any }): Promise<any> => {
|
|
258
|
+
const { object_name, record_id, record } = params;
|
|
259
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records/${record_id}`;
|
|
260
|
+
|
|
261
|
+
this.log(LoggerLevel.info, `[单条更新记录] 💾 开始更新 record_id: ${record_id}`);
|
|
262
|
+
|
|
263
|
+
const res = await functionLimiter(async () => {
|
|
264
|
+
await this.ensureTokenValid();
|
|
265
|
+
|
|
266
|
+
const response = await this.axiosInstance.patch(url, { record }, { headers: { Authorization: `${this.accessToken}` } });
|
|
267
|
+
|
|
268
|
+
this.log(LoggerLevel.info, `[单条更新记录] 💾 record_id: ${record_id} 更新完成,返回 code: ${response.data.code}`);
|
|
269
|
+
return response.data;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return res;
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 批量更新
|
|
277
|
+
* @param params 请求参数
|
|
278
|
+
* @returns 所有子请求的返回结果数组
|
|
279
|
+
*/
|
|
280
|
+
recordsBatchUpdate: async (params: { object_name: string; records: any[] }): Promise<any[]> => {
|
|
281
|
+
const { object_name, records } = params;
|
|
282
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records_batch`;
|
|
283
|
+
|
|
284
|
+
const chunkSize = 100;
|
|
285
|
+
const chunks: any[][] = [];
|
|
286
|
+
for (let i = 0; i < records.length; i += chunkSize) {
|
|
287
|
+
chunks.push(records.slice(i, i + chunkSize));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.log(LoggerLevel.info, `[批量更新记录] 💾 总共 ${records.length} 条记录,拆分为 ${chunks.length} 组,每组最多 ${chunkSize} 条`);
|
|
291
|
+
|
|
292
|
+
const results: any[] = [];
|
|
293
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
294
|
+
this.log(LoggerLevel.info, `[批量更新记录] 💾 开始更新第 ${index + 1} 组,共 ${chunk.length} 条`);
|
|
295
|
+
|
|
296
|
+
const res = await functionLimiter(async () => {
|
|
297
|
+
await this.ensureTokenValid();
|
|
298
|
+
|
|
299
|
+
const response = await this.axiosInstance.patch(url, { records: chunk }, { headers: { Authorization: `${this.accessToken}` } });
|
|
300
|
+
|
|
301
|
+
this.log(LoggerLevel.info, `[批量更新记录] 💾 第 ${index + 1} 组更新完成,返回 code: ${response.data.code}`);
|
|
302
|
+
return response.data;
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
results.push(res);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return results;
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
delete: {
|
|
313
|
+
/**
|
|
314
|
+
* 单条删除
|
|
315
|
+
* @param params 请求参数
|
|
316
|
+
* @returns 接口返回结果
|
|
317
|
+
*/
|
|
318
|
+
record: async (params: { object_name: string; record_id: string }): Promise<any> => {
|
|
319
|
+
const { object_name, record_id } = params;
|
|
320
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records/${record_id}`;
|
|
321
|
+
|
|
322
|
+
this.log(LoggerLevel.info, `[单条删除记录] 🗑️ 开始删除 record_id: ${record_id}`);
|
|
323
|
+
|
|
324
|
+
const res = await functionLimiter(async () => {
|
|
325
|
+
await this.ensureTokenValid();
|
|
326
|
+
|
|
327
|
+
const response = await this.axiosInstance.delete(url, {
|
|
328
|
+
headers: { Authorization: `${this.accessToken}` }
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
this.log(LoggerLevel.info, `[单条删除记录] 🗑️ record_id: ${record_id} 删除完成,返回 code: ${response.data.code}`);
|
|
332
|
+
return response.data;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return res;
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 批量删除
|
|
340
|
+
* @param params 请求参数
|
|
341
|
+
* @returns 所有子请求的返回结果数组
|
|
342
|
+
*/
|
|
343
|
+
recordsBatchDelete: async (params: { object_name: string; ids: string[] }): Promise<any[]> => {
|
|
344
|
+
const { object_name, ids } = params;
|
|
345
|
+
const url = `/v1/data/namespaces/${this.namespace}/objects/${object_name}/records_batch`;
|
|
346
|
+
|
|
347
|
+
const chunkSize = 100;
|
|
348
|
+
const chunks: string[][] = [];
|
|
349
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
350
|
+
chunks.push(ids.slice(i, i + chunkSize));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.log(LoggerLevel.info, `[批量删除记录] 🗑️ 总共 ${ids.length} 条记录,拆分为 ${chunks.length} 组,每组最多 ${chunkSize} 条`);
|
|
354
|
+
|
|
355
|
+
const results: any[] = [];
|
|
356
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
357
|
+
this.log(LoggerLevel.info, `[批量删除记录] 🗑️ 开始删除第 ${index + 1} 组,共 ${chunk.length} 条`);
|
|
358
|
+
|
|
359
|
+
const res = await functionLimiter(async () => {
|
|
360
|
+
await this.ensureTokenValid();
|
|
361
|
+
|
|
362
|
+
const response = await this.axiosInstance.delete(url, {
|
|
363
|
+
headers: { Authorization: `${this.accessToken}` },
|
|
364
|
+
data: { ids: chunk }
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
this.log(LoggerLevel.info, `[批量删除记录] 🗑️ 第 ${index + 1} 组删除完成,返回 code: ${response.data.code}`);
|
|
368
|
+
return response.data;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
results.push(res);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export const apaas = {
|
|
381
|
+
Client
|
|
382
|
+
};
|
package/src/limiter.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Bottleneck from 'bottleneck';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 默认 apaas 限流配置
|
|
5
|
+
*/
|
|
6
|
+
export const apaasLimiterOptions = {
|
|
7
|
+
minTime: 200, // 每秒最多发起 5 个数据库操作
|
|
8
|
+
reservoir: 20, // 最多同时查询 50 个数据库操作
|
|
9
|
+
reservoirRefreshAmount: 20, // 每次查询完毕后,重置为 50 个数据库操作
|
|
10
|
+
reservoirRefreshInterval: 1000 // 重置时间间隔为 1 秒
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 创建限流器
|
|
15
|
+
* @param fn 被限流函数
|
|
16
|
+
* @param options 自定义限流配置
|
|
17
|
+
* @returns 包装后的限流函数
|
|
18
|
+
*/
|
|
19
|
+
export async function functionLimiter<T>(fn: () => Promise<T>, options: Partial<Bottleneck.ConstructorOptions> = {}): Promise<T> {
|
|
20
|
+
const limiter = new Bottleneck({
|
|
21
|
+
minTime: options.minTime || apaasLimiterOptions.minTime,
|
|
22
|
+
reservoir: options.reservoir || apaasLimiterOptions.reservoir,
|
|
23
|
+
reservoirRefreshAmount: options.reservoirRefreshAmount || apaasLimiterOptions.reservoirRefreshAmount,
|
|
24
|
+
reservoirRefreshInterval: options.reservoirRefreshInterval || apaasLimiterOptions.reservoirRefreshInterval
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const wrapped = limiter.wrap(fn);
|
|
28
|
+
return wrapped();
|
|
29
|
+
}
|