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 +2 -42
- 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 +9 -5
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evaengine",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
243
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
442
|
-
|
|
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,
|
|
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
|
|
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';
|
|
@@ -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
|
+
};
|
package/src/utils/pagination.js
CHANGED
|
@@ -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
|
+
};
|