evaengine 1.0.2 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evaengine",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "EvaEngine for Node.js, a micro service development engine",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,8 +12,7 @@
12
12
  "scripts": {
13
13
  "prestart": "node src/index.js",
14
14
  "prepublish": "npm run test",
15
- "ava": "cross-env LOG_LEVEL=error NODE_ENV=test ava --verbose --serial",
16
- "test": "nyc -a --reporter=lcov --reporter=text --reporter=html npm run ava",
15
+ "test": "LOG_LEVEL=error NODE_ENV=test node --import ./test/bootstrap.js --test --test-concurrency=1 --experimental-test-coverage \"test/di.js\" \"test/engine.js\" \"test/error_handlers.js\" \"test/index.js\" \"test/exceptions/index.js\" \"test/entities/index.js\" \"test/commands/make_entity.js\" \"test/middlewares/*.js\" \"test/services/*.js\" \"test/swagger/index.js\" \"test/utils/*.js\"",
17
16
  "lint": "eslint src test index.js",
18
17
  "build": "node --check src/index.js",
19
18
  "ci:check": "npm run lint && npm run build",
@@ -38,31 +37,6 @@
38
37
  "lint"
39
38
  ],
40
39
  "author": "AlloVince",
41
- "ava": {
42
- "files": [
43
- "test/**/*.js",
44
- "!test/bootstrap.js",
45
- "!test/_*/**/*.js",
46
- "!test/swagger/_*/**/*.js"
47
- ],
48
- "source": [
49
- "src/**/*.js"
50
- ],
51
- "failFast": false,
52
- "tap": false,
53
- "require": [
54
- "./test/bootstrap.js"
55
- ]
56
- },
57
- "nyc": {
58
- "require": [],
59
- "include": [
60
- "src/**/*.js"
61
- ],
62
- "exclude": [
63
- "src/config/index.js"
64
- ]
65
- },
66
40
  "dependencies": {
67
41
  "acorn": "^8.18.0",
68
42
  "app-root-path": "^3.1.0",
@@ -73,24 +47,15 @@
73
47
  "doctrine": "^3.0.0",
74
48
  "express": "^5.2.1",
75
49
  "express-session": "^1.19.0",
76
- "glob": "^13.0.6",
77
50
  "ioredis": "^5.11.1",
78
51
  "joi": "^18.2.3",
79
52
  "js-yaml": "^5.2.2",
80
53
  "jwt-simple": "^0.5.6",
81
- "later": "^1.2.0",
82
54
  "lodash": "^4.18.1",
83
- "mkdirp": "^3.0.1",
84
- "moment": "^2.30.1",
85
55
  "moment-timezone": "^0.6.3",
86
56
  "morgan": "^1.11.0",
87
- "mysql": "^2.18.1",
88
57
  "mysql2": "^3.23.2",
89
58
  "node-mocks-http": "~1.18.1",
90
- "on-headers": "^1.1.0",
91
- "pug": "^3.0.4",
92
- "request": "^2.88.2",
93
- "request-promise-native": "^1.0.9",
94
59
  "sequelize": "^6.37.8",
95
60
  "swagger-ui-dist": "^5.32.11",
96
61
  "winston": "^3.19.0",
@@ -104,15 +69,10 @@
104
69
  "@semantic-release/github": "^11.0.0",
105
70
  "@semantic-release/npm": "^12.0.0",
106
71
  "@semantic-release/release-notes-generator": "^14.0.0",
107
- "ava": "^8.0.1",
108
72
  "conventional-changelog-conventionalcommits": "^8.0.0",
109
- "cross-env": "^10.1.0",
110
73
  "eslint": "^9.39.5",
111
74
  "eslint-plugin-import": "^2.32.0",
112
75
  "globals": "^17.8.0",
113
- "is-type-of": "^2.2.0",
114
- "nock": "^14.0.17",
115
- "nyc": "^18.0.0",
116
76
  "pre-commit": "^2.0.0",
117
77
  "semantic-release": "^25.0.8"
118
78
  }
@@ -1,7 +1,7 @@
1
- import _ from 'lodash';
1
+ import groupBy from 'lodash/groupBy.js';
2
+ import template from 'lodash/template.js';
2
3
  import Sequelize from 'sequelize';
3
4
  import fs from 'fs';
4
- import * as mkdirp from 'mkdirp';
5
5
  import Command from './interface.js';
6
6
  import DI from '../di.js';
7
7
  import Entities from '../entities/index.js';
@@ -55,7 +55,7 @@ CREATE ALGORITHM=UNDEFINED SQL SECURITY DEFINER VIEW view_${tableName}
55
55
  const entities = new Entities(entityPath);
56
56
 
57
57
  logger.info('Start generate db views to %s', file);
58
- mkdirp.sync(path);
58
+ fs.mkdirSync(path, { recursive: true });
59
59
  const models = Object.values(entities.getInstance().models);
60
60
  const sql = [];
61
61
  models.forEach((model) => {
@@ -202,7 +202,7 @@ export class MakeEntity extends Command {
202
202
  if (!rawIndexes) {
203
203
  return [];
204
204
  }
205
- rawIndexes = _.groupBy(rawIndexes, 'Key_name');
205
+ rawIndexes = groupBy(rawIndexes, 'Key_name');
206
206
  return Object.entries(rawIndexes).filter(([key]) => key !== 'PRIMARY').map(([name, columns]) => {
207
207
  const index = columns[0].Non_unique !== 1 ? { name, unique: true } : { name };
208
208
  index.fields = columns.map(c => c.Column_name);
@@ -239,8 +239,8 @@ export class MakeEntity extends Command {
239
239
  const schemaPath = `${path}/schemas`;
240
240
  const entityTemplate = fs.readFileSync(`${import.meta.dirname}/../../template/entity.ejs`, 'utf8');
241
241
  const schemaTemplate = fs.readFileSync(`${import.meta.dirname}/../../template/schema.ejs`, 'utf8');
242
- mkdirp.sync(path);
243
- mkdirp.sync(schemaPath);
242
+ fs.mkdirSync(path, { recursive: true });
243
+ fs.mkdirSync(schemaPath, { recursive: true });
244
244
 
245
245
  logger.info('Start generate DB schemas to dir %s', path);
246
246
 
@@ -269,7 +269,7 @@ export class MakeEntity extends Command {
269
269
  fs.accessSync(entityFile);
270
270
  logger.info('Entity file %s generate skipped, already exists by %s', table, entityFile);
271
271
  } catch {
272
- fs.writeFileSync(entityFile, _.template(entityTemplate)({ table }));
272
+ fs.writeFileSync(entityFile, template(entityTemplate)({ table }));
273
273
  logger.info('Entity file %s generated as %s', table, entityFile);
274
274
  }
275
275
 
@@ -279,7 +279,7 @@ export class MakeEntity extends Command {
279
279
  } catch {
280
280
  logger.info('Schema file %s generated as %s', table, schemaFile);
281
281
  }
282
- fs.writeFileSync(schemaFile, _.template(schemaTemplate)({
282
+ fs.writeFileSync(schemaFile, template(schemaTemplate)({
283
283
  columns,
284
284
  table,
285
285
  indexes,
@@ -438,8 +438,8 @@ export class MakeGraphql extends Command {
438
438
  const mappingContent = JSON.parse(fs.readFileSync(mappingFile, 'utf8'));
439
439
  const getMappedTableName = tableName =>
440
440
  (mappingContent[tableName] ? mappingContent[tableName] : tableName);
441
- mkdirp.sync(path);
442
- mkdirp.sync(schemaPath);
441
+ fs.mkdirSync(path, { recursive: true });
442
+ fs.mkdirSync(schemaPath, { recursive: true });
443
443
 
444
444
  logger.info('Start generate GraphQL schemas to dir %s', path);
445
445
 
@@ -474,7 +474,7 @@ export class MakeGraphql extends Command {
474
474
  } catch {
475
475
  logger.info('Graphql schema file %s generated as %s', tableName, schemaFile);
476
476
  }
477
- fs.writeFileSync(schemaFile, _.template(schemaTemplate)({
477
+ fs.writeFileSync(schemaFile, template(schemaTemplate)({
478
478
  tableName,
479
479
  mappedTableName,
480
480
  columns,
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[service]) {
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
- clearInterval(handler);
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
- this.logger.info('Cron job using %s Timezone', later.date.isUTC ? 'UTC' : 'Local');
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 = later.parse.cron(sequence, useSeconds);
527
- const handler = later.setInterval(async() => {
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); //第二个参数为True表示支持秒
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
- throw err;
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 {
@@ -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') {
@@ -1,4 +1,7 @@
1
- import _ from 'lodash';
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';
@@ -45,11 +48,11 @@ class Config extends ServiceInterface {
45
48
  const configRemote = await springConfigClient.load({
46
49
  endpoint,
47
50
  name,
48
- profiles: _.isString(profiles) ? profiles.split(',') : [],
51
+ profiles: isString(profiles) ? profiles.split(',') : [],
49
52
  label
50
53
  });
51
54
  configRemote.forEach((key, value) => {
52
- _.set(this.config, key, value);
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 _.merge({}, EngineConfig, configDefault, configEnv, configLocal);
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 _.get(target, keyString);
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 = request;
107
- requestDebug(logger);
16
+ this.client = createRequestClient(logger);
108
17
  }
109
18
 
110
19
  getProto() {
111
- return request;
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 _ from 'lodash';
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 (!_.get(this.config, 'kong.endpoint')) {
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
  }
@@ -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
  }
@@ -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 glob(path, options);
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
+ };
@@ -39,7 +39,7 @@ const toPaginationUrl = (query, req) => {
39
39
  }
40
40
  return toUrl(
41
41
  scheme, host, requestPath,
42
- Object.assign(req.query, query)
42
+ Object.assign({}, req.query, query)
43
43
  );
44
44
  };
45
45
 
@@ -0,0 +1,179 @@
1
+ /**
2
+ * A request-promise-native compatible HTTP client built on the global fetch.
3
+ *
4
+ * Keeps the option object and error shape used by the legacy `request`
5
+ * ecosystem so callers of HttpClient/RestClient see no interface change:
6
+ * - resolves with the response body (or the full response with
7
+ * `resolveWithFullResponse`)
8
+ * - rejects with an error carrying `.name` (`StatusCodeError`/`RequestError`),
9
+ * `.statusCode`, `.response` and `.request`
10
+ * - supports `url`, `method`, `headers`, `body`, `json`, `qs`, `form`,
11
+ * `formData`, `simple`, `resolveWithFullResponse` and `timeout`
12
+ */
13
+
14
+ const MAX_BODY_LENGTH = parseInt(process.env.MAX_REQUEST_DEBUG_BODY, 10) || 3000;
15
+ const TOO_LONG_BODY = '____TLDR____';
16
+
17
+ let debugId = 0;
18
+
19
+ const toPlainHeaders = (headers = {}) => {
20
+ const result = {};
21
+ Object.entries(headers).forEach(([key, value]) => {
22
+ result[key] = String(value);
23
+ });
24
+ return result;
25
+ };
26
+
27
+ const buildQueryString = (qs) => {
28
+ if (qs === undefined || qs === null) {
29
+ return '';
30
+ }
31
+ if (typeof qs === 'string') {
32
+ return qs.startsWith('?') ? qs : `?${qs}`;
33
+ }
34
+ return `?${new URLSearchParams(qs).toString()}`;
35
+ };
36
+
37
+ const buildBody = (params) => {
38
+ if (params.formData) {
39
+ const form = new FormData();
40
+ Object.entries(params.formData).forEach(([key, value]) => {
41
+ form.append(key, value);
42
+ });
43
+ return { body: form, contentType: null };
44
+ }
45
+ if (params.form) {
46
+ return {
47
+ body: new URLSearchParams(params.form).toString(),
48
+ contentType: 'application/x-www-form-urlencoded'
49
+ };
50
+ }
51
+ //The legacy `json` option also accepts a value used as the JSON body
52
+ if (params.json !== undefined && params.json !== false && params.json !== true) {
53
+ return { body: JSON.stringify(params.json), contentType: 'application/json' };
54
+ }
55
+ if (params.json === true && params.body !== undefined) {
56
+ return { body: JSON.stringify(params.body), contentType: 'application/json' };
57
+ }
58
+ return { body: params.body, contentType: null };
59
+ };
60
+
61
+ const truncateBody = (body) => {
62
+ if (typeof body !== 'string') {
63
+ return body ? String(body) : '';
64
+ }
65
+ return body.length > MAX_BODY_LENGTH ? TOO_LONG_BODY : body;
66
+ };
67
+
68
+ /**
69
+ * Create a request-promise-native compatible callable client.
70
+ * @param {Logger} [logger] when given, verbose request/response logs are emitted
71
+ * @returns {function}
72
+ */
73
+ export const createRequestClient = (logger) => {
74
+ const request = (params = {}) => new Promise((resolve, reject) => {
75
+ const {
76
+ url,
77
+ method: rawMethod = 'GET',
78
+ headers = {},
79
+ resolveWithFullResponse = false,
80
+ simple = true,
81
+ timeout = 0,
82
+ qs
83
+ } = params;
84
+
85
+ const method = String(rawMethod).toUpperCase();
86
+ const targetUrl = `${url}${buildQueryString(qs)}`;
87
+ const { body, contentType } = buildBody(params);
88
+
89
+ const requestHeaders = toPlainHeaders(headers);
90
+ if (contentType
91
+ && !Object.keys(requestHeaders).some(key => key.toLowerCase() === 'content-type')) {
92
+ requestHeaders['Content-Type'] = contentType;
93
+ }
94
+
95
+ const abortController = new AbortController();
96
+ const timer = timeout > 0
97
+ ? setTimeout(() => abortController.abort(new Error('Request timed out')), timeout)
98
+ : null;
99
+
100
+ const requestForDump = {
101
+ method,
102
+ uri: {
103
+ protocol: new URL(targetUrl).protocol,
104
+ href: targetUrl
105
+ },
106
+ headers: requestHeaders,
107
+ req: {},
108
+ _json: params.json !== undefined && params.json !== false
109
+ ? true : body !== undefined,
110
+ formData: params.formData || null,
111
+ body: params.formData
112
+ ? new URLSearchParams(params.formData).toString()
113
+ : body
114
+ };
115
+
116
+ const id = ++debugId;
117
+ if (logger) {
118
+ logger.verbose('[HTTP_REQUEST_%s] [%s %s] [REQ_HEADERS: %s] [REQ_BODY: %s]', id,
119
+ method, targetUrl, JSON.stringify(requestHeaders), truncateBody(body));
120
+ }
121
+
122
+ fetch(targetUrl, {
123
+ method,
124
+ headers: requestHeaders,
125
+ body,
126
+ signal: abortController.signal,
127
+ redirect: 'follow'
128
+ }).then(async (response) => {
129
+ if (timer) {
130
+ clearTimeout(timer);
131
+ }
132
+ const rawBody = await response.text();
133
+ let parsedBody = rawBody;
134
+ if (params.json !== undefined && params.json !== false) {
135
+ try {
136
+ parsedBody = JSON.parse(rawBody);
137
+ } catch {
138
+ parsedBody = rawBody;
139
+ }
140
+ }
141
+
142
+ const responseForDump = {
143
+ statusCode: response.status,
144
+ statusMessage: response.statusText,
145
+ headers: toPlainHeaders(Object.fromEntries(response.headers.entries())),
146
+ body: parsedBody,
147
+ request: requestForDump
148
+ };
149
+
150
+ if (logger) {
151
+ logger.verbose('[HTTP_RESPONSE_%s] [%s %s] [%s] [RES_HEADERS: %s] [RES_BODY: %s]', id,
152
+ method, targetUrl, response.status, JSON.stringify(responseForDump.headers),
153
+ truncateBody(parsedBody));
154
+ }
155
+
156
+ if (response.status >= 400 && simple) {
157
+ const error = new Error(`Response status ${response.status}`);
158
+ error.name = 'StatusCodeError';
159
+ error.statusCode = response.status;
160
+ error.response = responseForDump;
161
+ error.request = requestForDump;
162
+ reject(error);
163
+ return;
164
+ }
165
+
166
+ resolve(resolveWithFullResponse ? responseForDump : parsedBody);
167
+ }).catch((err) => {
168
+ if (timer) {
169
+ clearTimeout(timer);
170
+ }
171
+ const error = new Error(err.message);
172
+ error.name = 'RequestError';
173
+ error.request = requestForDump;
174
+ reject(error);
175
+ });
176
+ });
177
+
178
+ return request;
179
+ };