@jayfong/x-server 2.31.0 → 2.34.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/lib/_cjs/cli/api_generator.js +88 -58
- package/lib/_cjs/core/define_task.js +58 -42
- package/lib/_cjs/core/handler.js +11 -3
- package/lib/_cjs/core/server.js +2 -3
- package/lib/cli/api_generator.d.ts +1 -1
- package/lib/cli/api_generator.js +89 -59
- package/lib/core/define_task.js +58 -42
- package/lib/core/handler.d.ts +1 -1
- package/lib/core/handler.js +12 -5
- package/lib/core/server.js +2 -3
- package/lib/core/types.d.ts +11 -1
- package/package.json +1 -1
|
@@ -42,13 +42,14 @@ class ApiGenerator {
|
|
|
42
42
|
const program = moProject.getProgram().compilerObject;
|
|
43
43
|
const checker = program.getTypeChecker();
|
|
44
44
|
this.checker = checker;
|
|
45
|
-
const
|
|
45
|
+
const entrySourceFile = program.getSourceFile(entryFile);
|
|
46
|
+
const moduleSymbol = checker.getSymbolAtLocation(entrySourceFile);
|
|
46
47
|
const categorySymbols = checker.getExportsOfModule(moduleSymbol);
|
|
47
48
|
const apiData = [];
|
|
48
49
|
for (const categorySymbol of categorySymbols) {
|
|
49
50
|
var _ref, _categoryUrlMatch$;
|
|
50
51
|
// 分类的类型
|
|
51
|
-
const categoryType = checker.
|
|
52
|
+
const categoryType = checker.getTypeOfSymbol(categorySymbol);
|
|
52
53
|
|
|
53
54
|
// 处理器列表
|
|
54
55
|
const handlerSymbols = categoryType.getProperties();
|
|
@@ -64,11 +65,39 @@ class ApiGenerator {
|
|
|
64
65
|
let categoryUrl = (_ref = (_categoryUrlMatch$ = categoryUrlMatch[1]) != null ? _categoryUrlMatch$ : categoryUrlMatch[2]) != null ? _ref : '';
|
|
65
66
|
categoryUrl = categoryUrl === '' ? '/' : `/${categoryUrl}/`;
|
|
66
67
|
for (const handlerSymbol of handlerSymbols) {
|
|
67
|
-
//
|
|
68
|
-
|
|
68
|
+
// 处理器路径
|
|
69
|
+
let handlerPath = [handlerSymbol.getName()];
|
|
70
|
+
const handlerNode = handlerSymbol.getDeclarations()[0];
|
|
71
|
+
|
|
72
|
+
// 支持自定义 requestPath
|
|
73
|
+
if (
|
|
74
|
+
// const xx = defineHandler
|
|
75
|
+
_tsMorph.ts.isVariableDeclaration(handlerNode) && handlerNode.initializer &&
|
|
76
|
+
// defineHandler()
|
|
77
|
+
_tsMorph.ts.isCallExpression(handlerNode.initializer) &&
|
|
78
|
+
// defineHandler({})
|
|
79
|
+
_tsMorph.ts.isObjectLiteralExpression(handlerNode.initializer.arguments[0])) {
|
|
80
|
+
const props = handlerNode.initializer.arguments[0].properties;
|
|
81
|
+
const requestPathNode = props.find(item => {
|
|
82
|
+
var _item$name;
|
|
83
|
+
return ((_item$name = item.name) == null ? void 0 : _item$name.getText()) === 'requestPath';
|
|
84
|
+
});
|
|
85
|
+
if (requestPathNode &&
|
|
86
|
+
// requestPath: ''
|
|
87
|
+
_tsMorph.ts.isPropertyAssignment(requestPathNode)) {
|
|
88
|
+
const customHandlerPath =
|
|
89
|
+
// requestPath: 'xx'
|
|
90
|
+
_tsMorph.ts.isStringLiteral(requestPathNode.initializer) ? requestPathNode.initializer.text :
|
|
91
|
+
// requestPath: ['xx']
|
|
92
|
+
_tsMorph.ts.isArrayLiteralExpression(requestPathNode.initializer) ? requestPathNode.initializer.elements.filter(item => _tsMorph.ts.isStringLiteral(item)).map(item => item.text) : '';
|
|
93
|
+
if (customHandlerPath.length) {
|
|
94
|
+
handlerPath = (0, _vtils.castArray)(customHandlerPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
69
98
|
|
|
70
99
|
// 处理器 URL
|
|
71
|
-
const handlerUrl = `${categoryUrl}${
|
|
100
|
+
const handlerUrl = handlerPath.map(path => `${categoryUrl}${path}`);
|
|
72
101
|
|
|
73
102
|
// 处理器注释
|
|
74
103
|
const handlerComment = this.getComment(handlerSymbol);
|
|
@@ -77,11 +106,10 @@ class ApiGenerator {
|
|
|
77
106
|
continue;
|
|
78
107
|
}
|
|
79
108
|
this.debug('生成接口: %s ...', handlerUrl);
|
|
80
|
-
const handlerNode = handlerSymbol.getDeclarations()[0];
|
|
81
109
|
const handlerType = checker.getTypeAtLocation(handlerNode);
|
|
82
110
|
const [requestType, responseType, methodType] = checker.getTypeArguments(handlerType);
|
|
83
111
|
const handlerCategory = (0, _vtils.pascalCase)(categoryUrl) || 'Index';
|
|
84
|
-
const handlerDescription = handlerComment.description || handlerUrl;
|
|
112
|
+
const handlerDescription = handlerComment.description || handlerUrl.join(', ');
|
|
85
113
|
const handlerMethod = methodType.value;
|
|
86
114
|
const httpMethod = _http_method.HandlerMethodToHttpMethod[handlerMethod];
|
|
87
115
|
const requestData = this.getApiDto({
|
|
@@ -256,57 +284,59 @@ class ApiGenerator {
|
|
|
256
284
|
genYApiData(handles) {
|
|
257
285
|
const data = {};
|
|
258
286
|
for (const handle of handles) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
287
|
+
for (const path of handle.path) {
|
|
288
|
+
data[handle.category] = data[handle.category] || [];
|
|
289
|
+
data[handle.category].push(handle.handlerMethod === 'GET' ? {
|
|
290
|
+
method: handle.method.toUpperCase(),
|
|
291
|
+
title: handle.name,
|
|
292
|
+
path: path,
|
|
293
|
+
res_body_type: 'json',
|
|
294
|
+
req_body_is_json_schema: false,
|
|
295
|
+
res_body_is_json_schema: true,
|
|
296
|
+
req_params: [],
|
|
297
|
+
req_query: handle.requestData.children.map(item => ({
|
|
298
|
+
required: item.required ? 1 : 0,
|
|
299
|
+
name: item.name,
|
|
300
|
+
desc: item.desc,
|
|
301
|
+
type: 'string'
|
|
302
|
+
})),
|
|
303
|
+
req_headers: [],
|
|
304
|
+
req_body_form: [],
|
|
305
|
+
res_body: JSON.stringify(handle.responseDataJsonSchema)
|
|
306
|
+
} : handle.handlerMethod === 'FILE' ? {
|
|
307
|
+
method: handle.method.toUpperCase(),
|
|
308
|
+
title: handle.name,
|
|
309
|
+
path: path,
|
|
310
|
+
req_body_type: 'form',
|
|
311
|
+
res_body_type: 'json',
|
|
312
|
+
req_body_is_json_schema: false,
|
|
313
|
+
res_body_is_json_schema: true,
|
|
314
|
+
req_params: [],
|
|
315
|
+
req_query: [],
|
|
316
|
+
req_headers: [],
|
|
317
|
+
req_body_form: handle.requestData.children.map(item => ({
|
|
318
|
+
required: item.required ? 1 : 0,
|
|
319
|
+
name: item.name,
|
|
320
|
+
desc: item.desc,
|
|
321
|
+
type: item.type === 'file' ? 'file' : 'text'
|
|
322
|
+
})),
|
|
323
|
+
res_body: JSON.stringify(handle.responseDataJsonSchema)
|
|
324
|
+
} : {
|
|
325
|
+
method: handle.method.toUpperCase(),
|
|
326
|
+
title: handle.name,
|
|
327
|
+
path: path,
|
|
328
|
+
req_body_type: 'json',
|
|
329
|
+
res_body_type: 'json',
|
|
330
|
+
req_body_is_json_schema: true,
|
|
331
|
+
res_body_is_json_schema: true,
|
|
332
|
+
req_params: [],
|
|
333
|
+
req_query: [],
|
|
334
|
+
req_headers: [],
|
|
335
|
+
req_body_form: [],
|
|
336
|
+
req_body_other: JSON.stringify(handle.requestDataJsonSchema),
|
|
337
|
+
res_body: JSON.stringify(handle.responseDataJsonSchema)
|
|
338
|
+
});
|
|
339
|
+
}
|
|
310
340
|
}
|
|
311
341
|
return Object.keys(data).map(cat => ({
|
|
312
342
|
name: cat,
|
|
@@ -40,58 +40,74 @@ function defineSliceTask(options) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const count = parseInt(res[0][1], 10) + 1;
|
|
43
|
+
const addAction = async (data, addOptions) => {
|
|
44
|
+
const redisKeyPrefix = `${_x.x.appId}_batch_task_${options.name}`;
|
|
45
|
+
const key = (addOptions == null ? void 0 : addOptions.key) || '';
|
|
46
|
+
const duration = (addOptions == null ? void 0 : addOptions.duration) != null ? (0, _vtils.ms)(addOptions.duration) : typeof options.duration === 'function' ? (0, _vtils.ms)(options.duration(key)) : options.duration && (0, _vtils.ms)(options.duration);
|
|
47
|
+
const threshold = (addOptions == null ? void 0 : addOptions.threshold) || typeof options.threshold && (typeof options.threshold === 'function' ? options.threshold(key) : options.threshold);
|
|
48
|
+
const thresholdTimeout = (addOptions == null ? void 0 : addOptions.thresholdTimeout) != null ? (0, _vtils.ms)(addOptions.thresholdTimeout) : typeof options.thresholdTimeout === 'function' ? (0, _vtils.ms)(options.thresholdTimeout(key)) : options.thresholdTimeout && (0, _vtils.ms)(options.thresholdTimeout);
|
|
49
|
+
(0, _assert.default)(duration != null || threshold != null, '参数 threshold 和 duration 必须至少设置 1 个');
|
|
50
|
+
const redisKey = !key ? redisKeyPrefix : `${redisKeyPrefix}_${key}`;
|
|
51
|
+
const res = await _x.x.redis.multi([['llen', redisKey], ['lpush', redisKey, JSON.stringify(data)]]).exec();
|
|
52
|
+
const count = parseInt(res[0][1], 10) + 1;
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
54
|
+
// 仅时段
|
|
55
|
+
// 1分钟内的合并推送
|
|
56
|
+
if (duration != null) {
|
|
57
|
+
if (count === 1) {
|
|
58
|
+
await task.add({
|
|
59
|
+
key: key,
|
|
60
|
+
redisKey: redisKey
|
|
61
|
+
}, {
|
|
62
|
+
delay: duration
|
|
63
|
+
});
|
|
66
64
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
}
|
|
66
|
+
// 仅阈值
|
|
67
|
+
// 满10条推送
|
|
68
|
+
else if (threshold != null) {
|
|
69
|
+
const delayTaskId = redisKey;
|
|
70
|
+
const prevJob = await task.getJob(delayTaskId);
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// 到达阈值,立即发送
|
|
73
|
+
if (count === threshold) {
|
|
74
|
+
await (prevJob == null ? void 0 : prevJob.remove());
|
|
75
|
+
await task.add({
|
|
76
|
+
key: key,
|
|
77
|
+
redisKey: redisKey
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// 存在超时设置
|
|
81
|
+
else if (thresholdTimeout) {
|
|
82
|
+
if (!prevJob) {
|
|
76
83
|
await task.add({
|
|
77
84
|
key: key,
|
|
78
85
|
redisKey: redisKey
|
|
86
|
+
}, {
|
|
87
|
+
jobId: delayTaskId,
|
|
88
|
+
delay: thresholdTimeout
|
|
79
89
|
});
|
|
80
90
|
}
|
|
81
|
-
// 存在超时设置
|
|
82
|
-
else if (thresholdTimeout) {
|
|
83
|
-
if (!prevJob) {
|
|
84
|
-
await task.add({
|
|
85
|
-
key: key,
|
|
86
|
-
redisKey: redisKey
|
|
87
|
-
}, {
|
|
88
|
-
jobId: delayTaskId,
|
|
89
|
-
delay: thresholdTimeout
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
};
|
|
94
|
+
const addTask = defineTask({
|
|
95
|
+
name: `${options.name}_add`,
|
|
96
|
+
concurrency: 1,
|
|
97
|
+
handle: async payload => {
|
|
98
|
+
await addAction(payload.data, payload.options);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const res = {
|
|
102
|
+
add: async (data, addOptions) => {
|
|
103
|
+
const delay = addOptions.delay && (0, _vtils.ms)(addOptions.delay) || 0;
|
|
104
|
+
await addTask.add({
|
|
105
|
+
data: data,
|
|
106
|
+
options: addOptions
|
|
107
|
+
}, {
|
|
108
|
+
delay: delay
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
96
112
|
return res;
|
|
97
113
|
}
|
package/lib/_cjs/core/handler.js
CHANGED
|
@@ -10,8 +10,7 @@ var vae = _interopRequireWildcard(require("vtils/vae"));
|
|
|
10
10
|
var _validator = require("vtils/validator");
|
|
11
11
|
var _http_error = require("../core/http_error");
|
|
12
12
|
var _dispose = require("../services/dispose");
|
|
13
|
-
|
|
14
|
-
// prettier-ignore
|
|
13
|
+
var _server = require("./server");
|
|
15
14
|
_validator.yup.setLocale((0, _validator.getZhCN)({
|
|
16
15
|
getLabel: params => params.label || params.path
|
|
17
16
|
}));
|
|
@@ -79,9 +78,18 @@ class Handler {
|
|
|
79
78
|
'_#': _lzString.default.compress(JSON.stringify(res))
|
|
80
79
|
};
|
|
81
80
|
}
|
|
81
|
+
|
|
82
|
+
// 加密返回数据
|
|
83
|
+
if (this.options.responseDataEncrypt && _server.Server.options.responseEncryptAlgorithm) {
|
|
84
|
+
if (_server.Server.options.responseEncryptAlgorithm === 'simple') {
|
|
85
|
+
res = {
|
|
86
|
+
_$: (0, _vtils.rot13)((0, _vtils.base64UrlEncode)(`${Date.now()}${JSON.stringify(res)}`))
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
82
90
|
return res == null ? {} : res;
|
|
83
91
|
};
|
|
84
|
-
this.handleWs = async (
|
|
92
|
+
this.handleWs = async (_data, ctx) => {
|
|
85
93
|
const dispose = new _dispose.DisposeService();
|
|
86
94
|
ctx.ws.socket.on('message', async payload => {
|
|
87
95
|
try {
|
package/lib/_cjs/core/server.js
CHANGED
|
@@ -6,9 +6,9 @@ exports.Server = void 0;
|
|
|
6
6
|
var _interopRequireWildcard2 = _interopRequireDefault(require("@babel/runtime/helpers/interopRequireWildcard"));
|
|
7
7
|
var _fastify = _interopRequireDefault(require("fastify"));
|
|
8
8
|
var _vtils = require("vtils");
|
|
9
|
-
var _http_method = require("./http_method");
|
|
10
|
-
var _http_error = require("./http_error");
|
|
11
9
|
var _x = require("../x");
|
|
10
|
+
var _http_error = require("./http_error");
|
|
11
|
+
var _http_method = require("./http_method");
|
|
12
12
|
class Server {
|
|
13
13
|
constructor(options) {
|
|
14
14
|
this.options = options;
|
|
@@ -97,7 +97,6 @@ class Server {
|
|
|
97
97
|
if (handlerMethod === 'FILE') {
|
|
98
98
|
const part = await req.file();
|
|
99
99
|
files = Object.keys(part.fields).reduce((res, name) => {
|
|
100
|
-
;
|
|
101
100
|
res[name] = (0, _vtils.castArray)(part.fields[name]).map(item => item.file ? item : item.value)[0];
|
|
102
101
|
return res;
|
|
103
102
|
}, {});
|
package/lib/cli/api_generator.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import createDebug from 'debug';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import mo, { ts } from 'ts-morph';
|
|
5
|
-
import { ii, pascalCase } from 'vtils';
|
|
5
|
+
import { castArray, ii, pascalCase } from 'vtils';
|
|
6
6
|
import { HandlerMethodToHttpMethod } from "../core/http_method";
|
|
7
7
|
|
|
8
8
|
/** 注释 */
|
|
@@ -37,13 +37,14 @@ export class ApiGenerator {
|
|
|
37
37
|
const program = moProject.getProgram().compilerObject;
|
|
38
38
|
const checker = program.getTypeChecker();
|
|
39
39
|
this.checker = checker;
|
|
40
|
-
const
|
|
40
|
+
const entrySourceFile = program.getSourceFile(entryFile);
|
|
41
|
+
const moduleSymbol = checker.getSymbolAtLocation(entrySourceFile);
|
|
41
42
|
const categorySymbols = checker.getExportsOfModule(moduleSymbol);
|
|
42
43
|
const apiData = [];
|
|
43
44
|
for (const categorySymbol of categorySymbols) {
|
|
44
45
|
var _ref, _categoryUrlMatch$;
|
|
45
46
|
// 分类的类型
|
|
46
|
-
const categoryType = checker.
|
|
47
|
+
const categoryType = checker.getTypeOfSymbol(categorySymbol);
|
|
47
48
|
|
|
48
49
|
// 处理器列表
|
|
49
50
|
const handlerSymbols = categoryType.getProperties();
|
|
@@ -59,11 +60,39 @@ export class ApiGenerator {
|
|
|
59
60
|
let categoryUrl = (_ref = (_categoryUrlMatch$ = categoryUrlMatch[1]) != null ? _categoryUrlMatch$ : categoryUrlMatch[2]) != null ? _ref : '';
|
|
60
61
|
categoryUrl = categoryUrl === '' ? '/' : `/${categoryUrl}/`;
|
|
61
62
|
for (const handlerSymbol of handlerSymbols) {
|
|
62
|
-
//
|
|
63
|
-
|
|
63
|
+
// 处理器路径
|
|
64
|
+
let handlerPath = [handlerSymbol.getName()];
|
|
65
|
+
const handlerNode = handlerSymbol.getDeclarations()[0];
|
|
66
|
+
|
|
67
|
+
// 支持自定义 requestPath
|
|
68
|
+
if (
|
|
69
|
+
// const xx = defineHandler
|
|
70
|
+
ts.isVariableDeclaration(handlerNode) && handlerNode.initializer &&
|
|
71
|
+
// defineHandler()
|
|
72
|
+
ts.isCallExpression(handlerNode.initializer) &&
|
|
73
|
+
// defineHandler({})
|
|
74
|
+
ts.isObjectLiteralExpression(handlerNode.initializer.arguments[0])) {
|
|
75
|
+
const props = handlerNode.initializer.arguments[0].properties;
|
|
76
|
+
const requestPathNode = props.find(item => {
|
|
77
|
+
var _item$name;
|
|
78
|
+
return ((_item$name = item.name) == null ? void 0 : _item$name.getText()) === 'requestPath';
|
|
79
|
+
});
|
|
80
|
+
if (requestPathNode &&
|
|
81
|
+
// requestPath: ''
|
|
82
|
+
ts.isPropertyAssignment(requestPathNode)) {
|
|
83
|
+
const customHandlerPath =
|
|
84
|
+
// requestPath: 'xx'
|
|
85
|
+
ts.isStringLiteral(requestPathNode.initializer) ? requestPathNode.initializer.text :
|
|
86
|
+
// requestPath: ['xx']
|
|
87
|
+
ts.isArrayLiteralExpression(requestPathNode.initializer) ? requestPathNode.initializer.elements.filter(item => ts.isStringLiteral(item)).map(item => item.text) : '';
|
|
88
|
+
if (customHandlerPath.length) {
|
|
89
|
+
handlerPath = castArray(customHandlerPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
64
93
|
|
|
65
94
|
// 处理器 URL
|
|
66
|
-
const handlerUrl = `${categoryUrl}${
|
|
95
|
+
const handlerUrl = handlerPath.map(path => `${categoryUrl}${path}`);
|
|
67
96
|
|
|
68
97
|
// 处理器注释
|
|
69
98
|
const handlerComment = this.getComment(handlerSymbol);
|
|
@@ -72,11 +101,10 @@ export class ApiGenerator {
|
|
|
72
101
|
continue;
|
|
73
102
|
}
|
|
74
103
|
this.debug('生成接口: %s ...', handlerUrl);
|
|
75
|
-
const handlerNode = handlerSymbol.getDeclarations()[0];
|
|
76
104
|
const handlerType = checker.getTypeAtLocation(handlerNode);
|
|
77
105
|
const [requestType, responseType, methodType] = checker.getTypeArguments(handlerType);
|
|
78
106
|
const handlerCategory = pascalCase(categoryUrl) || 'Index';
|
|
79
|
-
const handlerDescription = handlerComment.description || handlerUrl;
|
|
107
|
+
const handlerDescription = handlerComment.description || handlerUrl.join(', ');
|
|
80
108
|
const handlerMethod = methodType.value;
|
|
81
109
|
const httpMethod = HandlerMethodToHttpMethod[handlerMethod];
|
|
82
110
|
const requestData = this.getApiDto({
|
|
@@ -251,57 +279,59 @@ export class ApiGenerator {
|
|
|
251
279
|
genYApiData(handles) {
|
|
252
280
|
const data = {};
|
|
253
281
|
for (const handle of handles) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
282
|
+
for (const path of handle.path) {
|
|
283
|
+
data[handle.category] = data[handle.category] || [];
|
|
284
|
+
data[handle.category].push(handle.handlerMethod === 'GET' ? {
|
|
285
|
+
method: handle.method.toUpperCase(),
|
|
286
|
+
title: handle.name,
|
|
287
|
+
path: path,
|
|
288
|
+
res_body_type: 'json',
|
|
289
|
+
req_body_is_json_schema: false,
|
|
290
|
+
res_body_is_json_schema: true,
|
|
291
|
+
req_params: [],
|
|
292
|
+
req_query: handle.requestData.children.map(item => ({
|
|
293
|
+
required: item.required ? 1 : 0,
|
|
294
|
+
name: item.name,
|
|
295
|
+
desc: item.desc,
|
|
296
|
+
type: 'string'
|
|
297
|
+
})),
|
|
298
|
+
req_headers: [],
|
|
299
|
+
req_body_form: [],
|
|
300
|
+
res_body: JSON.stringify(handle.responseDataJsonSchema)
|
|
301
|
+
} : handle.handlerMethod === 'FILE' ? {
|
|
302
|
+
method: handle.method.toUpperCase(),
|
|
303
|
+
title: handle.name,
|
|
304
|
+
path: path,
|
|
305
|
+
req_body_type: 'form',
|
|
306
|
+
res_body_type: 'json',
|
|
307
|
+
req_body_is_json_schema: false,
|
|
308
|
+
res_body_is_json_schema: true,
|
|
309
|
+
req_params: [],
|
|
310
|
+
req_query: [],
|
|
311
|
+
req_headers: [],
|
|
312
|
+
req_body_form: handle.requestData.children.map(item => ({
|
|
313
|
+
required: item.required ? 1 : 0,
|
|
314
|
+
name: item.name,
|
|
315
|
+
desc: item.desc,
|
|
316
|
+
type: item.type === 'file' ? 'file' : 'text'
|
|
317
|
+
})),
|
|
318
|
+
res_body: JSON.stringify(handle.responseDataJsonSchema)
|
|
319
|
+
} : {
|
|
320
|
+
method: handle.method.toUpperCase(),
|
|
321
|
+
title: handle.name,
|
|
322
|
+
path: path,
|
|
323
|
+
req_body_type: 'json',
|
|
324
|
+
res_body_type: 'json',
|
|
325
|
+
req_body_is_json_schema: true,
|
|
326
|
+
res_body_is_json_schema: true,
|
|
327
|
+
req_params: [],
|
|
328
|
+
req_query: [],
|
|
329
|
+
req_headers: [],
|
|
330
|
+
req_body_form: [],
|
|
331
|
+
req_body_other: JSON.stringify(handle.requestDataJsonSchema),
|
|
332
|
+
res_body: JSON.stringify(handle.responseDataJsonSchema)
|
|
333
|
+
});
|
|
334
|
+
}
|
|
305
335
|
}
|
|
306
336
|
return Object.keys(data).map(cat => ({
|
|
307
337
|
name: cat,
|
package/lib/core/define_task.js
CHANGED
|
@@ -34,58 +34,74 @@ export function defineSliceTask(options) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const count = parseInt(res[0][1], 10) + 1;
|
|
37
|
+
const addAction = async (data, addOptions) => {
|
|
38
|
+
const redisKeyPrefix = `${x.appId}_batch_task_${options.name}`;
|
|
39
|
+
const key = (addOptions == null ? void 0 : addOptions.key) || '';
|
|
40
|
+
const duration = (addOptions == null ? void 0 : addOptions.duration) != null ? ms(addOptions.duration) : typeof options.duration === 'function' ? ms(options.duration(key)) : options.duration && ms(options.duration);
|
|
41
|
+
const threshold = (addOptions == null ? void 0 : addOptions.threshold) || typeof options.threshold && (typeof options.threshold === 'function' ? options.threshold(key) : options.threshold);
|
|
42
|
+
const thresholdTimeout = (addOptions == null ? void 0 : addOptions.thresholdTimeout) != null ? ms(addOptions.thresholdTimeout) : typeof options.thresholdTimeout === 'function' ? ms(options.thresholdTimeout(key)) : options.thresholdTimeout && ms(options.thresholdTimeout);
|
|
43
|
+
assert(duration != null || threshold != null, '参数 threshold 和 duration 必须至少设置 1 个');
|
|
44
|
+
const redisKey = !key ? redisKeyPrefix : `${redisKeyPrefix}_${key}`;
|
|
45
|
+
const res = await x.redis.multi([['llen', redisKey], ['lpush', redisKey, JSON.stringify(data)]]).exec();
|
|
46
|
+
const count = parseInt(res[0][1], 10) + 1;
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
48
|
+
// 仅时段
|
|
49
|
+
// 1分钟内的合并推送
|
|
50
|
+
if (duration != null) {
|
|
51
|
+
if (count === 1) {
|
|
52
|
+
await task.add({
|
|
53
|
+
key: key,
|
|
54
|
+
redisKey: redisKey
|
|
55
|
+
}, {
|
|
56
|
+
delay: duration
|
|
57
|
+
});
|
|
60
58
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
}
|
|
60
|
+
// 仅阈值
|
|
61
|
+
// 满10条推送
|
|
62
|
+
else if (threshold != null) {
|
|
63
|
+
const delayTaskId = redisKey;
|
|
64
|
+
const prevJob = await task.getJob(delayTaskId);
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
// 到达阈值,立即发送
|
|
67
|
+
if (count === threshold) {
|
|
68
|
+
await (prevJob == null ? void 0 : prevJob.remove());
|
|
69
|
+
await task.add({
|
|
70
|
+
key: key,
|
|
71
|
+
redisKey: redisKey
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// 存在超时设置
|
|
75
|
+
else if (thresholdTimeout) {
|
|
76
|
+
if (!prevJob) {
|
|
70
77
|
await task.add({
|
|
71
78
|
key: key,
|
|
72
79
|
redisKey: redisKey
|
|
80
|
+
}, {
|
|
81
|
+
jobId: delayTaskId,
|
|
82
|
+
delay: thresholdTimeout
|
|
73
83
|
});
|
|
74
84
|
}
|
|
75
|
-
// 存在超时设置
|
|
76
|
-
else if (thresholdTimeout) {
|
|
77
|
-
if (!prevJob) {
|
|
78
|
-
await task.add({
|
|
79
|
-
key: key,
|
|
80
|
-
redisKey: redisKey
|
|
81
|
-
}, {
|
|
82
|
-
jobId: delayTaskId,
|
|
83
|
-
delay: thresholdTimeout
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
};
|
|
88
|
+
const addTask = defineTask({
|
|
89
|
+
name: `${options.name}_add`,
|
|
90
|
+
concurrency: 1,
|
|
91
|
+
handle: async payload => {
|
|
92
|
+
await addAction(payload.data, payload.options);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const res = {
|
|
96
|
+
add: async (data, addOptions) => {
|
|
97
|
+
const delay = addOptions.delay && ms(addOptions.delay) || 0;
|
|
98
|
+
await addTask.add({
|
|
99
|
+
data: data,
|
|
100
|
+
options: addOptions
|
|
101
|
+
}, {
|
|
102
|
+
delay: delay
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
90
106
|
return res;
|
|
91
107
|
}
|
package/lib/core/handler.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as vae from 'vtils/vae';
|
|
2
2
|
import type { XHandler } from './types';
|
|
3
|
-
export type { HandlerMethodMap, HandlerPath, HandlerPayloadMap, HandlerResultMap } from '.x/routes';
|
|
3
|
+
export type { HandlerMethodMap, HandlerPath, HandlerPayloadMap, HandlerResultMap, } from '.x/routes';
|
|
4
4
|
export declare class Handler<TReqData extends any = void, TResData extends any = void, TReqMethod extends XHandler.Method = XHandler.Method> {
|
|
5
5
|
readonly options: XHandler.Options<TReqData, TResData, TReqMethod>;
|
|
6
6
|
private requestDataSchema;
|
package/lib/core/handler.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import LZString from 'lz-string';
|
|
2
|
-
import { DataPacker } from 'vtils';
|
|
2
|
+
import { DataPacker, base64UrlEncode, rot13 } from 'vtils';
|
|
3
3
|
import * as vae from 'vtils/vae';
|
|
4
4
|
import { getZhCN, yup } from 'vtils/validator';
|
|
5
5
|
import { HttpError } from "../core/http_error";
|
|
6
6
|
import { DisposeService } from "../services/dispose";
|
|
7
|
-
|
|
8
|
-
// @ts-ignore
|
|
9
|
-
// prettier-ignore
|
|
7
|
+
import { Server } from "./server";
|
|
10
8
|
yup.setLocale(getZhCN({
|
|
11
9
|
getLabel: params => params.label || params.path
|
|
12
10
|
}));
|
|
@@ -74,9 +72,18 @@ export class Handler {
|
|
|
74
72
|
'_#': LZString.compress(JSON.stringify(res))
|
|
75
73
|
};
|
|
76
74
|
}
|
|
75
|
+
|
|
76
|
+
// 加密返回数据
|
|
77
|
+
if (this.options.responseDataEncrypt && Server.options.responseEncryptAlgorithm) {
|
|
78
|
+
if (Server.options.responseEncryptAlgorithm === 'simple') {
|
|
79
|
+
res = {
|
|
80
|
+
_$: rot13(base64UrlEncode(`${Date.now()}${JSON.stringify(res)}`))
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
77
84
|
return res == null ? {} : res;
|
|
78
85
|
};
|
|
79
|
-
this.handleWs = async (
|
|
86
|
+
this.handleWs = async (_data, ctx) => {
|
|
80
87
|
const dispose = new DisposeService();
|
|
81
88
|
ctx.ws.socket.on('message', async payload => {
|
|
82
89
|
try {
|
package/lib/core/server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
2
|
import { base64UrlDecode, castArray, keyBy, noop, rot13 } from 'vtils';
|
|
3
|
-
import { HandlerMethodToHttpMethod } from "./http_method";
|
|
4
|
-
import { HttpError } from "./http_error";
|
|
5
3
|
import { x } from "../x";
|
|
4
|
+
import { HttpError } from "./http_error";
|
|
5
|
+
import { HandlerMethodToHttpMethod } from "./http_method";
|
|
6
6
|
export class Server {
|
|
7
7
|
constructor(options) {
|
|
8
8
|
this.options = options;
|
|
@@ -91,7 +91,6 @@ export class Server {
|
|
|
91
91
|
if (handlerMethod === 'FILE') {
|
|
92
92
|
const part = await req.file();
|
|
93
93
|
files = Object.keys(part.fields).reduce((res, name) => {
|
|
94
|
-
;
|
|
95
94
|
res[name] = castArray(part.fields[name]).map(item => item.file ? item : item.value)[0];
|
|
96
95
|
return res;
|
|
97
96
|
}, {});
|
package/lib/core/types.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node/http" />
|
|
3
3
|
/// <reference types="got/dist/source/core/utils/timed-out" />
|
|
4
|
+
import type { IncomingHttpHeaders } from 'http';
|
|
4
5
|
import type { MultipartFile } from '@fastify/multipart';
|
|
5
6
|
import type { SocketStream } from '@fastify/websocket';
|
|
6
7
|
import type { Queue } from 'bull';
|
|
7
8
|
import { CronJob } from 'cron';
|
|
8
9
|
import { FastifyReply, FastifyRequest, FastifyServerOptions } from 'fastify';
|
|
9
|
-
import type { IncomingHttpHeaders } from 'http';
|
|
10
10
|
import type { MsValue } from 'vtils';
|
|
11
11
|
import type { AsyncOrSync, LiteralUnion, OneOrMore, RequiredDeep } from 'vtils/types';
|
|
12
12
|
import * as vae from 'vtils/vae';
|
|
@@ -36,6 +36,14 @@ export declare namespace XServer {
|
|
|
36
36
|
* 监听端口
|
|
37
37
|
*/
|
|
38
38
|
port: number;
|
|
39
|
+
/**
|
|
40
|
+
* 响应加密算法
|
|
41
|
+
*/
|
|
42
|
+
responseEncryptAlgorithm?: 'simple';
|
|
43
|
+
/**
|
|
44
|
+
* 响应加密密钥
|
|
45
|
+
*/
|
|
46
|
+
responseEncryptKey?: string;
|
|
39
47
|
/**
|
|
40
48
|
* 透传给 Fastify 的选项
|
|
41
49
|
*/
|
|
@@ -126,6 +134,7 @@ export declare namespace XHandler {
|
|
|
126
134
|
* @default false
|
|
127
135
|
*/
|
|
128
136
|
responseDataObfuscate?: boolean;
|
|
137
|
+
responseDataEncrypt?: boolean;
|
|
129
138
|
/**
|
|
130
139
|
* 响应的 Content-Type 头
|
|
131
140
|
*/
|
|
@@ -199,6 +208,7 @@ export declare namespace XTask {
|
|
|
199
208
|
duration?: MsValue;
|
|
200
209
|
threshold?: number;
|
|
201
210
|
thresholdTimeout?: MsValue;
|
|
211
|
+
delay?: MsValue;
|
|
202
212
|
}) => Promise<void>;
|
|
203
213
|
}
|
|
204
214
|
}
|