evaengine 1.0.2 → 1.0.4
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/.ai/defaults/ai-coding.md +47 -0
- package/.ai/defaults/preferences.md +45 -0
- package/.ai/memory.md +38 -0
- package/.ai/workflow/design-review.md +38 -0
- package/.ai/workflow/end.md +34 -0
- package/.ai/workflow/start.md +34 -0
- package/.ai/workflow/sync.md +24 -0
- package/README.md +384 -54
- package/bin/engine +1 -1
- package/package.json +2 -44
- package/src/commands/make_entity.js +11 -11
- package/src/di.js +13 -1
- package/src/engine.js +12 -9
- package/src/index.js +1 -3
- package/src/middlewares/session.js +6 -1
- package/src/middlewares/trace.js +26 -1
- package/src/middlewares/view_cache.js +3 -2
- package/src/services/config.js +10 -6
- package/src/services/http_client.js +3 -94
- package/src/services/jwt_token_kong.js +2 -2
- package/src/services/redis.js +9 -1
- package/src/swagger/index.js +1 -6
- package/src/utils/cron.js +208 -0
- package/src/utils/pagination.js +1 -1
- package/src/utils/request_client.js +179 -0
- package/.ai/analysis.md +0 -657
- package/.ai/architecture/runtime.md +0 -504
- package/.ai/contracts/public-api.md +0 -447
- package/.ai/external-project-guide.md +0 -248
- package/.ai/vision.md +0 -314
package/src/di.js
CHANGED
|
@@ -4,6 +4,11 @@ import { ServiceProvider } from './services/providers.js';
|
|
|
4
4
|
|
|
5
5
|
let container = new constitute.Container();
|
|
6
6
|
let bound = {};
|
|
7
|
+
let boundKind = {};
|
|
8
|
+
|
|
9
|
+
const BIND_CLASS = 'class';
|
|
10
|
+
const BIND_VALUE = 'value';
|
|
11
|
+
const BIND_METHOD = 'method';
|
|
7
12
|
|
|
8
13
|
export default class DI {
|
|
9
14
|
static getContainer() {
|
|
@@ -19,15 +24,19 @@ export default class DI {
|
|
|
19
24
|
return container.constitute(service);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
if (!bound
|
|
27
|
+
if (!Object.prototype.hasOwnProperty.call(bound, service)) {
|
|
23
28
|
throw new RuntimeException(`Service ${service} not bound yet`);
|
|
24
29
|
}
|
|
30
|
+
if (boundKind[service] === BIND_VALUE) {
|
|
31
|
+
return bound[service];
|
|
32
|
+
}
|
|
25
33
|
return container.constitute(bound[service]);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
static bindClass(...args) {
|
|
29
37
|
if (typeof args[0] === 'string') {
|
|
30
38
|
[, bound[args[0]]] = args;
|
|
39
|
+
boundKind[args[0]] = BIND_CLASS;
|
|
31
40
|
}
|
|
32
41
|
return container.bindClass(...args);
|
|
33
42
|
}
|
|
@@ -35,6 +44,7 @@ export default class DI {
|
|
|
35
44
|
static bindValue(...args) {
|
|
36
45
|
if (typeof args[0] === 'string') {
|
|
37
46
|
[, bound[args[0]]] = args;
|
|
47
|
+
boundKind[args[0]] = BIND_VALUE;
|
|
38
48
|
}
|
|
39
49
|
return container.bindValue(...args);
|
|
40
50
|
}
|
|
@@ -42,6 +52,7 @@ export default class DI {
|
|
|
42
52
|
static bindMethod(...args) {
|
|
43
53
|
if (typeof args[0] === 'string') {
|
|
44
54
|
bound[args[0]] = args[0];
|
|
55
|
+
boundKind[args[0]] = BIND_METHOD;
|
|
45
56
|
}
|
|
46
57
|
return container.bindMethod(...args);
|
|
47
58
|
}
|
|
@@ -49,6 +60,7 @@ export default class DI {
|
|
|
49
60
|
static reset() {
|
|
50
61
|
container = new constitute.Container();
|
|
51
62
|
bound = {};
|
|
63
|
+
boundKind = {};
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/**
|
package/src/engine.js
CHANGED
|
@@ -3,13 +3,13 @@ import http from 'http';
|
|
|
3
3
|
import https from 'https';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import yargs from 'yargs/yargs';
|
|
6
|
-
import later from 'later';
|
|
7
6
|
import moment from 'moment-timezone';
|
|
8
7
|
import packageJson from '../package.json' with { type: 'json' };
|
|
9
8
|
import DI from './di.js';
|
|
10
9
|
import * as ServiceProviders from './services/providers.js';
|
|
11
10
|
import * as MiddlewareProviders from './middlewares/providers.js';
|
|
12
11
|
import { StandardException, RuntimeException } from './exceptions/index.js';
|
|
12
|
+
import { parseCron, setCronInterval } from './utils/cron.js';
|
|
13
13
|
|
|
14
14
|
moment.tz.setDefault(process.env.TZ ? process.env.TZ : 'Asia/Shanghai');
|
|
15
15
|
|
|
@@ -21,8 +21,7 @@ export const MODES = {
|
|
|
21
21
|
export {
|
|
22
22
|
DI,
|
|
23
23
|
express,
|
|
24
|
-
yargs
|
|
25
|
-
later
|
|
24
|
+
yargs
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
let app = null;
|
|
@@ -116,7 +115,6 @@ export default class EvaEngine {
|
|
|
116
115
|
this.namespace = namespace || DI.get('namespace');
|
|
117
116
|
this.logger.info('Engine started, Meta:', this.meta);
|
|
118
117
|
this.logger.debug('Engine config files loaded:', this.config.getMergedFiles());
|
|
119
|
-
later.date.localTime();
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
/**
|
|
@@ -301,7 +299,11 @@ export default class EvaEngine {
|
|
|
301
299
|
|
|
302
300
|
clearCrontabs() {
|
|
303
301
|
this.crontabJobHandlers.forEach((handler) => {
|
|
304
|
-
|
|
302
|
+
if (handler && typeof handler.clear === 'function') {
|
|
303
|
+
handler.clear();
|
|
304
|
+
} else {
|
|
305
|
+
clearInterval(handler);
|
|
306
|
+
}
|
|
305
307
|
});
|
|
306
308
|
this.crontabJobHandlers = [];
|
|
307
309
|
}
|
|
@@ -514,7 +516,8 @@ export default class EvaEngine {
|
|
|
514
516
|
}
|
|
515
517
|
this.registerServiceProviders(EvaEngine.getServiceProvidersForCLI());
|
|
516
518
|
this.logger.debug('Bound services', Object.keys(DI.getBound()));
|
|
517
|
-
|
|
519
|
+
//Cron jobs run in local time, matching the previous later.date.localTime() default
|
|
520
|
+
this.logger.info('Cron job using %s Timezone', 'Local');
|
|
518
521
|
const [commandName, ...options] = commandString.split(' ');
|
|
519
522
|
if (Object.keys(this.commands).includes(commandName) === false) {
|
|
520
523
|
throw new RuntimeException(`Command ${commandName} not registered`);
|
|
@@ -523,14 +526,14 @@ export default class EvaEngine {
|
|
|
523
526
|
const command = new this.commands[commandName](argv);
|
|
524
527
|
|
|
525
528
|
let i = 1;
|
|
526
|
-
const schedule =
|
|
527
|
-
const handler =
|
|
529
|
+
const schedule = parseCron(sequence, useSeconds);
|
|
530
|
+
const handler = setCronInterval(async() => {
|
|
528
531
|
this.logger.info('Cron job [%s] | Round %d | started with params %j', commandName, i, argv);
|
|
529
532
|
//Let job crash if any exception happen
|
|
530
533
|
await command.run();
|
|
531
534
|
this.logger.info('Cron job [%s] | Round %d | finished', commandName, i);
|
|
532
535
|
i += 1;
|
|
533
|
-
}, schedule);
|
|
536
|
+
}, schedule); //useSeconds 为 True 时启用秒级字段
|
|
534
537
|
this.logger.info('Cron job [%s] with sequence [%s] registered as %j', commandString, sequence, schedule);
|
|
535
538
|
this.crontabJobHandlers.push(handler);
|
|
536
539
|
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import constitute from 'constitute';
|
|
2
|
-
import * as mysql from 'mysql';
|
|
3
2
|
import sequelize from 'sequelize';
|
|
4
3
|
import Joi from 'joi';
|
|
5
4
|
import EvaEngine, * as engine from './engine.js';
|
|
@@ -39,8 +38,7 @@ const core = {
|
|
|
39
38
|
dependencies: {
|
|
40
39
|
Joi,
|
|
41
40
|
constitute,
|
|
42
|
-
sequelize
|
|
43
|
-
mysql
|
|
41
|
+
sequelize
|
|
44
42
|
},
|
|
45
43
|
exceptions,
|
|
46
44
|
middlewares,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import session from 'express-session';
|
|
2
2
|
import { RedisStore } from 'connect-redis';
|
|
3
3
|
import constitute from 'constitute';
|
|
4
|
+
import DI from '../di.js';
|
|
4
5
|
import Config from '../services/config.js';
|
|
5
6
|
import Redis from '../services/redis.js';
|
|
6
7
|
import Namespace from '../services/namespace.js';
|
|
@@ -33,7 +34,11 @@ function SessionMiddleware(_config, redis, namespace) {
|
|
|
33
34
|
client: config.store.client || redis.getInstance()
|
|
34
35
|
}));
|
|
35
36
|
RedisClient.client.on('error', (err) => {
|
|
36
|
-
|
|
37
|
+
try {
|
|
38
|
+
DI.get('logger').error('Session Redis store error:', err);
|
|
39
|
+
} catch {
|
|
40
|
+
console.error('Session Redis store error:', err);
|
|
41
|
+
}
|
|
37
42
|
});
|
|
38
43
|
store = RedisClient;
|
|
39
44
|
} else {
|
package/src/middlewares/trace.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import onHeaders from 'on-headers';
|
|
2
1
|
import constitute from 'constitute';
|
|
3
2
|
import { randomString, getHostFullUrl, getHostPort, getHostIp, getMicroTimestamp } from '../utils/index.js';
|
|
4
3
|
import Namespace from '../services/namespace.js';
|
|
@@ -8,6 +7,32 @@ import HttpClient from '../services/http_client.js';
|
|
|
8
7
|
|
|
9
8
|
const hostIp = getHostIp();
|
|
10
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Execute a listener when a response is about to write headers.
|
|
12
|
+
* Inlined from the on-headers package (MIT), limited to the behavior trace middleware needs.
|
|
13
|
+
* @param {object} res
|
|
14
|
+
* @param {function} listener
|
|
15
|
+
*/
|
|
16
|
+
const onHeaders = (res, listener) => {
|
|
17
|
+
if (!res) {
|
|
18
|
+
throw new TypeError('argument res is required');
|
|
19
|
+
}
|
|
20
|
+
if (typeof listener !== 'function') {
|
|
21
|
+
throw new TypeError('argument listener must be a function');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const prevWriteHead = res.writeHead;
|
|
25
|
+
let fired = false;
|
|
26
|
+
|
|
27
|
+
res.writeHead = function writeHead() {
|
|
28
|
+
if (!fired) {
|
|
29
|
+
fired = true;
|
|
30
|
+
listener.call(this);
|
|
31
|
+
}
|
|
32
|
+
return prevWriteHead.apply(this, arguments);
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
11
36
|
export const tracerToZipkins = (tracer) => {
|
|
12
37
|
if (!tracer) {
|
|
13
38
|
return false;
|
|
@@ -24,9 +24,10 @@ export const requestToCacheKey = (req, hashStrategy) => {
|
|
|
24
24
|
method,
|
|
25
25
|
originalUrl,
|
|
26
26
|
query: originQuery,
|
|
27
|
-
route
|
|
28
|
-
uid = null //Custom rule
|
|
27
|
+
route
|
|
29
28
|
} = req;
|
|
29
|
+
//Prefer uid resolved by auth middleware, keep req.uid as custom rule fallback
|
|
30
|
+
const uid = (req.auth && req.auth.uid) || req.uid || null;
|
|
30
31
|
const query = { ...originQuery };
|
|
31
32
|
delete query.flush;
|
|
32
33
|
if (hashStrategy && typeof hashStrategy !== 'function') {
|
package/src/services/config.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import get from 'lodash/get.js';
|
|
2
|
+
import isString from 'lodash/isString.js';
|
|
3
|
+
import merge from 'lodash/merge.js';
|
|
4
|
+
import set from 'lodash/set.js';
|
|
2
5
|
import { createRequire } from 'module';
|
|
3
6
|
import constitute from 'constitute';
|
|
4
7
|
import * as springConfigClient from 'cloud-config-client';
|
|
@@ -37,7 +40,7 @@ class Config extends ServiceInterface {
|
|
|
37
40
|
endpoint,
|
|
38
41
|
name,
|
|
39
42
|
profiles,
|
|
40
|
-
label = '
|
|
43
|
+
label = 'main'
|
|
41
44
|
}) {
|
|
42
45
|
if (!this.config) {
|
|
43
46
|
this.config = this.loadConfigFromFiles();
|
|
@@ -45,11 +48,11 @@ class Config extends ServiceInterface {
|
|
|
45
48
|
const configRemote = await springConfigClient.load({
|
|
46
49
|
endpoint,
|
|
47
50
|
name,
|
|
48
|
-
profiles:
|
|
51
|
+
profiles: isString(profiles) ? profiles.split(',') : [],
|
|
49
52
|
label
|
|
50
53
|
});
|
|
51
54
|
configRemote.forEach((key, value) => {
|
|
52
|
-
|
|
55
|
+
set(this.config, key, value);
|
|
53
56
|
});
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -82,7 +85,7 @@ class Config extends ServiceInterface {
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
|
|
85
|
-
return
|
|
88
|
+
return merge({}, EngineConfig, configDefault, configEnv, configLocal);
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
getMergedFiles() {
|
|
@@ -91,6 +94,7 @@ class Config extends ServiceInterface {
|
|
|
91
94
|
|
|
92
95
|
reload() {
|
|
93
96
|
this.config = null;
|
|
97
|
+
this.mergedFiles = [];
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
/**
|
|
@@ -102,7 +106,7 @@ class Config extends ServiceInterface {
|
|
|
102
106
|
if (typeof keyString !== 'string') {
|
|
103
107
|
return target;
|
|
104
108
|
}
|
|
105
|
-
return
|
|
109
|
+
return get(target, keyString);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
@@ -1,100 +1,10 @@
|
|
|
1
1
|
import constitute from 'constitute';
|
|
2
|
-
import request from 'request-promise-native';
|
|
3
2
|
import Config from './config.js';
|
|
4
3
|
import Logger from './logger.js';
|
|
4
|
+
import { createRequestClient } from '../utils/request_client.js';
|
|
5
5
|
import { HttpRequestLogicException, HttpRequestIOException } from '../exceptions/index.js';
|
|
6
6
|
import ServiceInterface from './interface.js';
|
|
7
7
|
|
|
8
|
-
export const deepClone = obj =>
|
|
9
|
-
JSON.parse(JSON.stringify(obj));
|
|
10
|
-
|
|
11
|
-
const TOO_LONG_BODY = '____TLDR____';
|
|
12
|
-
|
|
13
|
-
let debugFlag = false;
|
|
14
|
-
/* eslint-disable */
|
|
15
|
-
//Mod version of https://github.com/request/request-debug
|
|
16
|
-
export const requestDebug = (logger, maxBodyLength = process.env.MAX_REQUEST_DEBUG_BODY || 3000) => {
|
|
17
|
-
if (debugFlag === true) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let proto = {};
|
|
22
|
-
let debugId = 0;
|
|
23
|
-
if (request.Request) {
|
|
24
|
-
proto = request.Request.prototype;
|
|
25
|
-
} else if (request.get && request.post) {
|
|
26
|
-
// The object returned by request.defaults() doesn't include the
|
|
27
|
-
// Request property, so do this horrible thing to get at it. Per
|
|
28
|
-
// Wikipedia, port 4 is unassigned.
|
|
29
|
-
const req = request('http://localhost:4').on('error', () => {
|
|
30
|
-
});
|
|
31
|
-
proto = req.constructor.prototype;
|
|
32
|
-
} else {
|
|
33
|
-
throw new Error('Pass the object returned by require("request") to this function.');
|
|
34
|
-
}
|
|
35
|
-
if (proto._initBeforeDebug) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
proto._initBeforeDebug = proto.init;
|
|
40
|
-
|
|
41
|
-
const ignoreDebug = (headers) => {
|
|
42
|
-
if (!headers || Object.keys(headers).length < 1) {
|
|
43
|
-
return {};
|
|
44
|
-
}
|
|
45
|
-
return Object.keys(headers)
|
|
46
|
-
.filter(key => !key.startsWith('x-debug'))
|
|
47
|
-
.reduce((res, key) => (res[key] = headers[key], res), {});
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
proto.init = function () {
|
|
51
|
-
if (this._debugId) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
this.on('request', function (req) {
|
|
55
|
-
const data = {
|
|
56
|
-
headers: deepClone(this.headers)
|
|
57
|
-
};
|
|
58
|
-
if (this.body) {
|
|
59
|
-
data.body = maxBodyLength > 0 && this.body.toString().length < maxBodyLength ? this.body.toString('utf8') : TOO_LONG_BODY;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
logger.verbose('[HTTP_REQUEST_%s] [%s %s] [REQ_HEADERS: %s] [REQ_BODY: %s]', this._debugId,
|
|
63
|
-
this.method.toUpperCase(), this.uri.href, JSON.stringify(data.headers), data.body || ''
|
|
64
|
-
);
|
|
65
|
-
}).on('response', function (res) {
|
|
66
|
-
if (this.callback) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const bodyLength = res.headers['content-length'] || (res.body ? res.body.toString().length : 0);
|
|
71
|
-
logger.verbose('[HTTP_RESPONSE_%s] [%s %s] [%s] [RES_HEADERS: %s] [RES_BODY: %s]', this._debugId,
|
|
72
|
-
this.method.toUpperCase(), this.uri.href, res.statusCode, JSON.stringify(ignoreDebug(res.headers)),
|
|
73
|
-
bodyLength > maxBodyLength ? TOO_LONG_BODY : res.body || ''
|
|
74
|
-
);
|
|
75
|
-
}).on('complete', function (res) {
|
|
76
|
-
if (!this.callback) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const bodyLength = res.headers['content-length'] || (res.body ? res.body.toString().length : 0);
|
|
81
|
-
logger.verbose('[HTTP_RESPONSE_%s] [%s %s] [%s] [RES_HEADERS: %s] [RES_BODY: %s]', this._debugId,
|
|
82
|
-
this.method.toUpperCase(), this.uri.href, res.statusCode, JSON.stringify(ignoreDebug(res.headers)),
|
|
83
|
-
bodyLength > maxBodyLength ? TOO_LONG_BODY : res.body || ''
|
|
84
|
-
);
|
|
85
|
-
}).on('redirect', function () {
|
|
86
|
-
|
|
87
|
-
logger.verbose('[HTTP_REDIRECT_%s] [%s %s] [%s] [RES_HEADERS: %s] [RES_BODY: %s]', this._debugId,
|
|
88
|
-
this.method.toUpperCase(), this.uri.href, this.response.statusCode, JSON.stringify(ignoreDebug(this.response.headers)), '');
|
|
89
|
-
});
|
|
90
|
-
this._debugId = ++debugId;
|
|
91
|
-
return proto._initBeforeDebug.apply(this, arguments);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
debugFlag = true;
|
|
95
|
-
};
|
|
96
|
-
/* eslint-enable */
|
|
97
|
-
|
|
98
8
|
class HttpClient extends ServiceInterface {
|
|
99
9
|
/**
|
|
100
10
|
* @param config {Config}
|
|
@@ -103,12 +13,11 @@ class HttpClient extends ServiceInterface {
|
|
|
103
13
|
constructor(config, logger) {
|
|
104
14
|
super();
|
|
105
15
|
this.config = config.get();
|
|
106
|
-
this.client =
|
|
107
|
-
requestDebug(logger);
|
|
16
|
+
this.client = createRequestClient(logger);
|
|
108
17
|
}
|
|
109
18
|
|
|
110
19
|
getProto() {
|
|
111
|
-
return
|
|
20
|
+
return this.client;
|
|
112
21
|
}
|
|
113
22
|
|
|
114
23
|
getInstance() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import constitute from 'constitute';
|
|
2
2
|
import jwt from 'jwt-simple';
|
|
3
|
-
import
|
|
3
|
+
import get from 'lodash/get.js';
|
|
4
4
|
|
|
5
5
|
import { RuntimeException } from '../exceptions/index.js';
|
|
6
6
|
import Config from './config.js';
|
|
@@ -16,7 +16,7 @@ class KongJsonWebToken extends ServiceInterface {
|
|
|
16
16
|
super();
|
|
17
17
|
this.restClient = restClient;
|
|
18
18
|
this.config = config.get('token');
|
|
19
|
-
if (!
|
|
19
|
+
if (!get(this.config, 'kong.endpoint')) {
|
|
20
20
|
throw new RuntimeException('config item `token.kong.endpoint` can not be null');
|
|
21
21
|
}
|
|
22
22
|
}
|
package/src/services/redis.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Ioredis from 'ioredis';
|
|
2
2
|
import constitute from 'constitute';
|
|
3
|
+
import DI from '../di.js';
|
|
3
4
|
import Config from './config.js';
|
|
4
5
|
import ServiceInterface from './interface.js';
|
|
5
6
|
|
|
@@ -45,7 +46,14 @@ class Redis extends ServiceInterface {
|
|
|
45
46
|
this.client = new Ioredis(Object.assign({
|
|
46
47
|
enableOfflineQueue: true //make redis connect failings throw error
|
|
47
48
|
}, this.options || this.config.get('redis')));
|
|
48
|
-
this.client.on('error', () => {
|
|
49
|
+
this.client.on('error', (err) => {
|
|
50
|
+
try {
|
|
51
|
+
DI.get('logger').error('Redis client error:', err);
|
|
52
|
+
} catch {
|
|
53
|
+
//Logger not bound yet, keep the error visible on stderr
|
|
54
|
+
console.error('Redis client error:', err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
49
57
|
return this.client;
|
|
50
58
|
}
|
|
51
59
|
}
|
package/src/swagger/index.js
CHANGED
|
@@ -6,7 +6,6 @@ import assert from 'assert';
|
|
|
6
6
|
import merge from 'lodash/merge.js';
|
|
7
7
|
import * as doctrine from 'doctrine';
|
|
8
8
|
import * as acorn from 'acorn';
|
|
9
|
-
import { glob } from 'glob';
|
|
10
9
|
import * as yaml from 'js-yaml';
|
|
11
10
|
import Entitles from '../entities/index.js';
|
|
12
11
|
import { RuntimeException, StandardException } from '../exceptions/index.js';
|
|
@@ -161,10 +160,6 @@ export class Fragment {
|
|
|
161
160
|
};
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
if (!this.isUnknown()) {
|
|
165
|
-
return {};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
163
|
if (this.isException()) {
|
|
169
164
|
if (!this.getParentFragment()) {
|
|
170
165
|
return {};
|
|
@@ -417,7 +412,7 @@ export class ExSwagger {
|
|
|
417
412
|
* @returns {Promise|Array.<string>}
|
|
418
413
|
*/
|
|
419
414
|
static async scanFiles(path, options = {}) {
|
|
420
|
-
return
|
|
415
|
+
return fs.globSync(path, options);
|
|
421
416
|
}
|
|
422
417
|
|
|
423
418
|
/**
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal cron expression parser and scheduler.
|
|
3
|
+
* Replaces the unmaintained `later` package with Node built-ins.
|
|
4
|
+
*
|
|
5
|
+
* Supports 5-field (minute hour day-of-month month day-of-week) and
|
|
6
|
+
* 6-field (with a leading seconds) expressions. Each field accepts
|
|
7
|
+
* wildcard, step, single value, range, range-with-step and
|
|
8
|
+
* comma-separated lists. All times are interpreted in local time,
|
|
9
|
+
* matching the previous `later.date.localTime()` default.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const FIELD_BOUNDS = {
|
|
13
|
+
second: [0, 59],
|
|
14
|
+
minute: [0, 59],
|
|
15
|
+
hour: [0, 23],
|
|
16
|
+
dayOfMonth: [1, 31],
|
|
17
|
+
month: [1, 12],
|
|
18
|
+
dayOfWeek: [0, 7]
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const FIELD_ORDER = [
|
|
22
|
+
'second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const parseField = (field, min, max) => {
|
|
26
|
+
const values = new Set();
|
|
27
|
+
for (const token of field.split(',')) {
|
|
28
|
+
if (token === '*') {
|
|
29
|
+
for (let value = min; value <= max; value += 1) {
|
|
30
|
+
values.add(value);
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (token.includes('/')) {
|
|
35
|
+
const [base, stepToken] = token.split('/');
|
|
36
|
+
const step = Number.parseInt(stepToken, 10);
|
|
37
|
+
if (Number.isNaN(step) || step < 1) {
|
|
38
|
+
throw new Error(`Invalid cron step "${stepToken}" in "${field}"`);
|
|
39
|
+
}
|
|
40
|
+
let start = min;
|
|
41
|
+
let end = max;
|
|
42
|
+
if (base !== '*') {
|
|
43
|
+
if (base.includes('-')) {
|
|
44
|
+
[start, end] = base.split('-').map(v => Number.parseInt(v, 10));
|
|
45
|
+
} else {
|
|
46
|
+
start = Number.parseInt(base, 10);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (Number.isNaN(start) || start < min || start > max
|
|
50
|
+
|| Number.isNaN(end) || end < min || end > max || start > end) {
|
|
51
|
+
throw new Error(`Invalid cron value in "${field}"`);
|
|
52
|
+
}
|
|
53
|
+
for (let value = start; value <= end; value += step) {
|
|
54
|
+
values.add(value);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (token.includes('-')) {
|
|
59
|
+
const [start, end] = token.split('-').map(v => Number.parseInt(v, 10));
|
|
60
|
+
if (Number.isNaN(start) || Number.isNaN(end)
|
|
61
|
+
|| start < min || end > max || start > end) {
|
|
62
|
+
throw new Error(`Invalid cron range in "${field}"`);
|
|
63
|
+
}
|
|
64
|
+
for (let value = start; value <= end; value += 1) {
|
|
65
|
+
values.add(value);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const value = Number.parseInt(token, 10);
|
|
70
|
+
if (Number.isNaN(value) || value < min || value > max) {
|
|
71
|
+
throw new Error(`Invalid cron value "${token}" in "${field}"`);
|
|
72
|
+
}
|
|
73
|
+
values.add(value);
|
|
74
|
+
}
|
|
75
|
+
return values;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse a cron expression into a schedule of Sets.
|
|
80
|
+
* @param {string} expression
|
|
81
|
+
* @param {boolean} [useSeconds=false]
|
|
82
|
+
* @returns {Object}
|
|
83
|
+
*/
|
|
84
|
+
export const parseCron = (expression, useSeconds = false) => {
|
|
85
|
+
const parts = String(expression).trim().split(/\s+/);
|
|
86
|
+
const names = useSeconds ? FIELD_ORDER : FIELD_ORDER.slice(1);
|
|
87
|
+
if (parts.length !== names.length) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Cron expression must contain ${names.length} fields, got ${parts.length}: "${expression}"`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const schedule = {};
|
|
93
|
+
names.forEach((name, index) => {
|
|
94
|
+
const [min, max] = FIELD_BOUNDS[name];
|
|
95
|
+
schedule[name] = parseField(parts[index], min, max);
|
|
96
|
+
});
|
|
97
|
+
if (schedule.dayOfWeek) {
|
|
98
|
+
if (schedule.dayOfWeek.has(7)) {
|
|
99
|
+
schedule.dayOfWeek.delete(7);
|
|
100
|
+
schedule.dayOfWeek.add(0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return schedule;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const matches = (schedule, date) => {
|
|
107
|
+
if (schedule.second && !schedule.second.has(date.getSeconds())) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (schedule.minute && !schedule.minute.has(date.getMinutes())) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (schedule.hour && !schedule.hour.has(date.getHours())) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (schedule.dayOfMonth && !schedule.dayOfMonth.has(date.getDate())) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (schedule.month && !schedule.month.has(date.getMonth() + 1)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (schedule.dayOfWeek && !schedule.dayOfWeek.has(date.getDay())) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compute the next run date strictly after `from` that matches the schedule.
|
|
130
|
+
* @param {Object} schedule
|
|
131
|
+
* @param {Date} [from=new Date()]
|
|
132
|
+
* @returns {Date}
|
|
133
|
+
*/
|
|
134
|
+
export const getNextRun = (schedule, from = new Date()) => {
|
|
135
|
+
const stepMs = schedule.second ? 1000 : 60000;
|
|
136
|
+
let current = new Date(Math.floor(from.getTime() / stepMs) * stepMs + stepMs);
|
|
137
|
+
const deadline = from.getTime() + 366 * 24 * 3600 * 1000;
|
|
138
|
+
|
|
139
|
+
while (current.getTime() <= deadline) {
|
|
140
|
+
if (matches(schedule, current)) {
|
|
141
|
+
return current;
|
|
142
|
+
}
|
|
143
|
+
//Fast-forward coarse fields so unschedulable expressions stay fast
|
|
144
|
+
if (schedule.month && !schedule.month.has(current.getMonth() + 1)) {
|
|
145
|
+
current = new Date(current.getFullYear(), current.getMonth() + 1, 1, 0, 0, 0, 0);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (schedule.dayOfMonth && !schedule.dayOfMonth.has(current.getDate())) {
|
|
149
|
+
current.setDate(current.getDate() + 1);
|
|
150
|
+
current.setHours(0, 0, 0, 0);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (schedule.dayOfWeek && !schedule.dayOfWeek.has(current.getDay())) {
|
|
154
|
+
current.setDate(current.getDate() + 1);
|
|
155
|
+
current.setHours(0, 0, 0, 0);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (schedule.hour && !schedule.hour.has(current.getHours())) {
|
|
159
|
+
current.setHours(current.getHours() + 1, 0, 0, 0);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (schedule.minute && !schedule.minute.has(current.getMinutes())) {
|
|
163
|
+
current.setMinutes(current.getMinutes() + 1, 0, 0);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (schedule.second && !schedule.second.has(current.getSeconds())) {
|
|
167
|
+
current.setSeconds(current.getSeconds() + 1, 0);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
current = new Date(current.getTime() + stepMs);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new Error('Cron expression has no match within the next year');
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const MAX_TIMEOUT = 2147483647;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Run `callback` on `schedule` until the returned handle is cleared.
|
|
180
|
+
* @param {function} callback
|
|
181
|
+
* @param {Object} schedule
|
|
182
|
+
* @returns {{clear: function}}
|
|
183
|
+
*/
|
|
184
|
+
export const setCronInterval = (callback, schedule) => {
|
|
185
|
+
let timer = null;
|
|
186
|
+
|
|
187
|
+
const scheduleNext = () => {
|
|
188
|
+
const nextRun = getNextRun(schedule, new Date());
|
|
189
|
+
const delay = Math.max(0, nextRun.getTime() - Date.now());
|
|
190
|
+
timer = setTimeout(() => {
|
|
191
|
+
scheduleNext();
|
|
192
|
+
if (delay <= MAX_TIMEOUT) {
|
|
193
|
+
callback();
|
|
194
|
+
}
|
|
195
|
+
}, Math.min(delay, MAX_TIMEOUT));
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
scheduleNext();
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
clear() {
|
|
202
|
+
if (timer) {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
timer = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
};
|