mythix 1.0.1 → 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/package.json +10 -7
- package/spec/controller-utils-spec.js +1 -1
- package/src/application.js +24 -8
- package/src/cli/cli-utils.js +5 -0
- package/src/cli/migrations/migrate-command.js +7 -3
- package/src/cli/routes-command.js +39 -0
- package/src/controllers/controller-base.js +34 -3
- package/src/controllers/controller-utils.js +25 -5
- package/src/http-server/http-errors.js +16 -3
- package/src/http-server/http-server.js +127 -42
- package/src/index.js +18 -15
- package/src/logger.js +26 -12
- package/src/models/model-utils.js +99 -22
- package/src/models/model.js +114 -18
- package/src/tasks/task-base.js +1 -1
- package/src/tasks/task-utils.js +1 -1
- package/src/utils/http-utils.js +5 -4
- package/src/utils/index.js +2 -0
- package/src/utils/test-utils.js +161 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mythix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Mythix is a NodeJS web-app framework",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -23,12 +23,15 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"chokidar": "^3.5.3",
|
|
25
25
|
"deep-diff": "^1.0.2",
|
|
26
|
-
"express": "^4.17.
|
|
27
|
-
"express-busboy": "^8.0.
|
|
28
|
-
"inflection": "^1.13.
|
|
26
|
+
"express": "^4.17.3",
|
|
27
|
+
"express-busboy": "^8.0.2",
|
|
28
|
+
"inflection": "^1.13.2",
|
|
29
29
|
"lodash": "^4.17.21",
|
|
30
|
-
"nife": "^1.
|
|
31
|
-
"object-hash": "^
|
|
32
|
-
"sequelize": "^6.
|
|
30
|
+
"nife": "^1.6.0",
|
|
31
|
+
"object-hash": "^3.0.0",
|
|
32
|
+
"sequelize": "^6.18.0",
|
|
33
|
+
"sqlite3": "^4.2.0",
|
|
34
|
+
"pg": "^8.7.3",
|
|
35
|
+
"pg-hstore": "^2.3.4"
|
|
33
36
|
}
|
|
34
37
|
}
|
package/src/application.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
} = require('./utils');
|
|
15
15
|
|
|
16
16
|
function nowInSeconds() {
|
|
17
|
-
return
|
|
17
|
+
return Date.now() / 1000;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Trace what is requesting the application exit
|
|
@@ -58,6 +58,7 @@ class Application extends EventEmitter {
|
|
|
58
58
|
autoReload: (process.env.NODE_ENV || 'development') === 'development',
|
|
59
59
|
exitOnShutdown: null,
|
|
60
60
|
runTasks: true,
|
|
61
|
+
testMode: false,
|
|
61
62
|
}, _opts || {});
|
|
62
63
|
|
|
63
64
|
Object.defineProperties(this, {
|
|
@@ -138,7 +139,7 @@ class Application extends EventEmitter {
|
|
|
138
139
|
enumerable: false,
|
|
139
140
|
configurable: true,
|
|
140
141
|
value: this.createLogger(
|
|
141
|
-
Object.assign({},
|
|
142
|
+
Object.assign({}, this.getConfigValue('logger', {}), opts.logger || {}),
|
|
142
143
|
Logger,
|
|
143
144
|
),
|
|
144
145
|
},
|
|
@@ -777,17 +778,22 @@ class Application extends EventEmitter {
|
|
|
777
778
|
}
|
|
778
779
|
|
|
779
780
|
var sequelize = new Sequelize(databaseConfig);
|
|
781
|
+
var dbConnectionString;
|
|
782
|
+
|
|
783
|
+
if (Nife.instanceOf(databaseConfig, 'string'))
|
|
784
|
+
dbConnectionString = databaseConfig;
|
|
785
|
+
else
|
|
786
|
+
dbConnectionString = `${databaseConfig.dialect}://${databaseConfig.host}:${databaseConfig.port || '<default port>'}/${databaseConfig.database}`;
|
|
780
787
|
|
|
781
|
-
var dbConnectionString = `${databaseConfig.dialect}://${databaseConfig.host}:${databaseConfig.port || '<default port>'}/${databaseConfig.database}`;
|
|
782
788
|
try {
|
|
783
789
|
await sequelize.authenticate();
|
|
784
790
|
|
|
785
|
-
this.getLogger().
|
|
791
|
+
this.getLogger().info(`Connection to ${dbConnectionString} has been established successfully!`);
|
|
786
792
|
|
|
787
793
|
return sequelize;
|
|
788
794
|
} catch (error) {
|
|
789
795
|
this.getLogger().error(`Unable to connect to database ${dbConnectionString}:`, error);
|
|
790
|
-
|
|
796
|
+
throw error;
|
|
791
797
|
}
|
|
792
798
|
}
|
|
793
799
|
|
|
@@ -803,6 +809,13 @@ class Application extends EventEmitter {
|
|
|
803
809
|
return server;
|
|
804
810
|
}
|
|
805
811
|
|
|
812
|
+
getDBTablePrefix(userSpecifiedPrefix) {
|
|
813
|
+
if (Nife.isNotEmpty(userSpecifiedPrefix))
|
|
814
|
+
return userSpecifiedPrefix;
|
|
815
|
+
|
|
816
|
+
return `${this.getApplicationName()}_`;
|
|
817
|
+
}
|
|
818
|
+
|
|
806
819
|
async start() {
|
|
807
820
|
var options = this.getOptions();
|
|
808
821
|
|
|
@@ -818,10 +831,13 @@ class Application extends EventEmitter {
|
|
|
818
831
|
return;
|
|
819
832
|
}
|
|
820
833
|
|
|
821
|
-
|
|
834
|
+
if (options.testMode) {
|
|
835
|
+
databaseConfig.logging = false;
|
|
836
|
+
} else {
|
|
837
|
+
databaseConfig.logging = (this.getLogger().isDebugLevel()) ? this.getLogger().log.bind(this.getLogger()) : false;
|
|
838
|
+
}
|
|
822
839
|
|
|
823
|
-
|
|
824
|
-
databaseConfig.tablePrefix = `${this.getApplicationName()}_`;
|
|
840
|
+
databaseConfig.tablePrefix = this.getDBTablePrefix(databaseConfig.tablePrefix);
|
|
825
841
|
|
|
826
842
|
this.dbConnection = await this.connectToDatabase(databaseConfig);
|
|
827
843
|
|
package/src/cli/cli-utils.js
CHANGED
|
@@ -34,6 +34,11 @@ class CommandBase {
|
|
|
34
34
|
var app = this.getApplication();
|
|
35
35
|
return app.getLogger();
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
getDBConnection() {
|
|
39
|
+
var application = this.getApplication();
|
|
40
|
+
return application.getDBConnection();
|
|
41
|
+
}
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
var loadingAllCommandsInProgress = false;
|
|
@@ -63,7 +63,11 @@ module.exports = defineCommand('migrate', ({ Parent }) => {
|
|
|
63
63
|
return doneCallback();
|
|
64
64
|
|
|
65
65
|
var migrationFileName = migrationFiles[index];
|
|
66
|
-
|
|
66
|
+
|
|
67
|
+
if (rollback)
|
|
68
|
+
console.log(`Undoing migration ${migrationFileName}...`);
|
|
69
|
+
else
|
|
70
|
+
console.log(`Running migration ${migrationFileName}...`);
|
|
67
71
|
|
|
68
72
|
MigrationUtils.executeMigration(queryInterface, migrationFileName, useTransaction, 0, rollback).then(
|
|
69
73
|
() => nextMigration(doneCallback, index + 1),
|
|
@@ -78,10 +82,10 @@ module.exports = defineCommand('migrate', ({ Parent }) => {
|
|
|
78
82
|
var applicationOptions = application.getOptions();
|
|
79
83
|
var migrationsPath = applicationOptions.migrationsPath;
|
|
80
84
|
var migrationFiles = this.getMigrationFiles(migrationsPath);
|
|
81
|
-
var useTransaction =
|
|
85
|
+
var useTransaction = args.transaction;
|
|
82
86
|
var rollback = args.rollback;
|
|
83
87
|
|
|
84
|
-
console.log('USING TRANSACTION: ', useTransaction, args['
|
|
88
|
+
console.log('USING TRANSACTION: ', useTransaction, args['transaction'], rollback, typeof rollback);
|
|
85
89
|
|
|
86
90
|
if (args.revision)
|
|
87
91
|
migrationFiles = this.getMigrationFilesFromRevision(migrationFiles, args.revision);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { defineCommand } = require('./cli-utils');
|
|
2
|
+
const { Logger } = require('../logger');
|
|
3
|
+
|
|
4
|
+
module.exports = defineCommand('routes', ({ Parent }) => {
|
|
5
|
+
return class RoutesCommand extends Parent {
|
|
6
|
+
static description = 'List application routes';
|
|
7
|
+
static applicationConfig = { logger: { level: Logger.ERROR } };
|
|
8
|
+
|
|
9
|
+
execute(args) {
|
|
10
|
+
const whitespaceOfLength = (len) => {
|
|
11
|
+
if (len < 0)
|
|
12
|
+
return '';
|
|
13
|
+
|
|
14
|
+
var parts = new Array(len);
|
|
15
|
+
for (var i = 0, il = parts.length; i < il; i++)
|
|
16
|
+
parts[i] = ' ';
|
|
17
|
+
|
|
18
|
+
return parts.join('');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const stringToLength = (str, len) => {
|
|
22
|
+
return `${str}${whitespaceOfLength(len - str.length)}`
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
var application = this.getApplication();
|
|
26
|
+
var routes = application.buildRoutes(null, application.getRoutes());
|
|
27
|
+
|
|
28
|
+
routes.forEach((route) => {
|
|
29
|
+
var methods = route.methods;
|
|
30
|
+
if (!methods === '*')
|
|
31
|
+
methods = [ '{ANY}' ];
|
|
32
|
+
|
|
33
|
+
methods.forEach((method) => {
|
|
34
|
+
console.log(`${stringToLength(method, 8)}${route.path} -> ${route.controller}`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const Nife
|
|
1
|
+
const Nife = require('nife');
|
|
2
|
+
const HTTPErrors = require('../http-server/http-errors');
|
|
2
3
|
|
|
3
4
|
class ControllerBase {
|
|
4
5
|
constructor(application, logger, request, response) {
|
|
@@ -27,6 +28,12 @@ class ControllerBase {
|
|
|
27
28
|
configurable: true,
|
|
28
29
|
value: logger,
|
|
29
30
|
},
|
|
31
|
+
'route': {
|
|
32
|
+
writable: true,
|
|
33
|
+
enumberable: false,
|
|
34
|
+
configurable: true,
|
|
35
|
+
value: null,
|
|
36
|
+
},
|
|
30
37
|
});
|
|
31
38
|
}
|
|
32
39
|
|
|
@@ -54,8 +61,31 @@ class ControllerBase {
|
|
|
54
61
|
return application.getModels();
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
getDBConnection() {
|
|
65
|
+
var application = this.getApplication();
|
|
66
|
+
return application.getDBConnection();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throwNotFoundError(message) {
|
|
70
|
+
throw new HTTPErrors.HTTPNotFoundError(this.route, message);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throwBadRequestError(message) {
|
|
74
|
+
throw new HTTPErrors.HTTPBadRequestError(this.route, message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throwUnauthorizedError(message) {
|
|
78
|
+
throw new HTTPErrors.HTTPUnauthorizedError(this.route, message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throwInternalServerError(message) {
|
|
82
|
+
throw new HTTPErrors.HTTPInternalServerError(this.route, message);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async handleIncomingRequest(request, response, { route, controller, controllerMethod, controllerInstance, startTime, params, query }) {
|
|
86
|
+
this.route = route;
|
|
87
|
+
|
|
88
|
+
return await this[controllerMethod].call(this, { params, query, body: request.body, request, response, startTime }, this.getModels());
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
async handleOutgoingResponse(_controllerResult, request, response, { route, controller, controllerMethod, controllerInstance, startTime, params }) {
|
|
@@ -74,6 +104,7 @@ class ControllerBase {
|
|
|
74
104
|
if (controllerResult == null)
|
|
75
105
|
controllerResult = {};
|
|
76
106
|
|
|
107
|
+
response.header('Content-Type', 'application/json; charset=UTF-8');
|
|
77
108
|
response.status(200).send(JSON.stringify(controllerResult));
|
|
78
109
|
}
|
|
79
110
|
}
|
|
@@ -20,8 +20,9 @@ const ROUTE_PROPERTIES = [
|
|
|
20
20
|
'accept',
|
|
21
21
|
'controller',
|
|
22
22
|
'methods',
|
|
23
|
-
'
|
|
23
|
+
'middleware',
|
|
24
24
|
'priority',
|
|
25
|
+
'queryParams',
|
|
25
26
|
];
|
|
26
27
|
|
|
27
28
|
function buildPatternMatcher(_patterns, _opts) {
|
|
@@ -281,6 +282,26 @@ function getRouteProperties(route) {
|
|
|
281
282
|
function compileRoutes(routes, customParserTypes, _context) {
|
|
282
283
|
const sortRoutes = (routes) => {
|
|
283
284
|
return routes.sort((a, b) => {
|
|
285
|
+
var pathA = a.path;
|
|
286
|
+
var pathB = b.path;
|
|
287
|
+
|
|
288
|
+
if (Nife.arrayUnion(a.methods, b.methods).length > 0) {
|
|
289
|
+
// If the routes are at the same level
|
|
290
|
+
// but one of them has a capture parameter
|
|
291
|
+
// then the one without a capture parameter comes first.
|
|
292
|
+
// If we don't do this, then the capture parameter wild-card
|
|
293
|
+
// might match the route name of the non-parameter route
|
|
294
|
+
var lastForwardSlashIndexA = pathA.lastIndexOf('/');
|
|
295
|
+
var lastForwardSlashIndexB = pathB.lastIndexOf('/');
|
|
296
|
+
|
|
297
|
+
if (lastForwardSlashIndexA >= 0 && lastForwardSlashIndexB >= 0 && pathA.substring(0, lastForwardSlashIndexA) === pathB.substring(0, lastForwardSlashIndexB)) {
|
|
298
|
+
if (pathA.indexOf('<', lastForwardSlashIndexA) >= 0 && pathB.indexOf('<', lastForwardSlashIndexB) < 0)
|
|
299
|
+
return 1;
|
|
300
|
+
else if (pathA.indexOf('<', lastForwardSlashIndexA) < 0 && pathB.indexOf('<', lastForwardSlashIndexB) >= 0)
|
|
301
|
+
return -1;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
284
305
|
var x = a.priority;
|
|
285
306
|
var y = b.priority;
|
|
286
307
|
|
|
@@ -349,9 +370,6 @@ function compileRoutes(routes, customParserTypes, _context) {
|
|
|
349
370
|
if (!path)
|
|
350
371
|
path = '/';
|
|
351
372
|
|
|
352
|
-
if (routeName !== '/')
|
|
353
|
-
path = `${(path)}/${routeName}`;
|
|
354
|
-
|
|
355
373
|
path = path.replace(/\/{2,}/g, '/');
|
|
356
374
|
|
|
357
375
|
var keys = Object.keys(routes);
|
|
@@ -370,10 +388,12 @@ function compileRoutes(routes, customParserTypes, _context) {
|
|
|
370
388
|
continue;
|
|
371
389
|
|
|
372
390
|
var thisRouteName = (isArray) ? routeName : key;
|
|
391
|
+
var newPath = (isArray) ? path : `${path}/${key}`;
|
|
392
|
+
|
|
373
393
|
if (Nife.instanceOf(route, 'object', 'array')) {
|
|
374
394
|
var subRoutes = compileRoutes(route, customParserTypes, {
|
|
375
395
|
routeName: thisRouteName,
|
|
376
|
-
path:
|
|
396
|
+
path: newPath,
|
|
377
397
|
priority: i,
|
|
378
398
|
depth: depth + 1,
|
|
379
399
|
alreadyVisited,
|
|
@@ -25,10 +25,19 @@ class HTTPBadRequestError extends HTTPBaseError {
|
|
|
25
25
|
constructor(route, message) {
|
|
26
26
|
super(route, message, 400);
|
|
27
27
|
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class HTTPBadContentTypeError extends HTTPBaseError {
|
|
31
|
+
constructor(route, message) {
|
|
32
|
+
super(route, message, 400);
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
getMessage() {
|
|
30
|
-
var route
|
|
31
|
-
|
|
36
|
+
var route = this.route;
|
|
37
|
+
if (!route)
|
|
38
|
+
return this.message;
|
|
39
|
+
|
|
40
|
+
var accept = route.accept;
|
|
32
41
|
if (!(accept instanceof Array))
|
|
33
42
|
accept = [ accept ];
|
|
34
43
|
|
|
@@ -43,7 +52,10 @@ class HTTPBadRequestError extends HTTPBaseError {
|
|
|
43
52
|
return `'${part}'`;
|
|
44
53
|
});
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
if (this.message)
|
|
56
|
+
return `${this.message}: Accepted Content-Types are [ ${accept.join(', ')} ]`;
|
|
57
|
+
else
|
|
58
|
+
return `Accepted Content-Types are [ ${accept.join(', ')} ]`;
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
61
|
|
|
@@ -63,6 +75,7 @@ module.exports = {
|
|
|
63
75
|
HTTPBaseError,
|
|
64
76
|
HTTPNotFoundError,
|
|
65
77
|
HTTPBadRequestError,
|
|
78
|
+
HTTPBadContentTypeError,
|
|
66
79
|
HTTPUnauthorizedError,
|
|
67
80
|
HTTPInternalServerError,
|
|
68
81
|
};
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
HTTPBaseError,
|
|
13
13
|
HTTPNotFoundError,
|
|
14
14
|
HTTPBadRequestError,
|
|
15
|
+
HTTPBadContentTypeError,
|
|
15
16
|
HTTPInternalServerError,
|
|
16
17
|
} = require('./http-errors');
|
|
17
18
|
|
|
@@ -23,7 +24,7 @@ class HTTPServer {
|
|
|
23
24
|
constructor(application, _opts) {
|
|
24
25
|
var appName = application.getApplicationName();
|
|
25
26
|
|
|
26
|
-
var uploadPath = Path.resolve(OS.tmpdir(),
|
|
27
|
+
var uploadPath = Path.resolve(OS.tmpdir(), appName.replace(/[^\w-]/g, ''), ('' + process.pid));
|
|
27
28
|
|
|
28
29
|
var opts = Nife.extend(true, {
|
|
29
30
|
host: 'localhost',
|
|
@@ -102,46 +103,67 @@ class HTTPServer {
|
|
|
102
103
|
this.routes = routes;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
executeMiddleware(middleware, request, response) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
if (Nife.isEmpty(middleware))
|
|
109
|
+
return resolve();
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
var application = this.getApplication();
|
|
112
|
+
if (!request.mythixApplication)
|
|
113
|
+
request.mythixApplication = application;
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
var logger = request.mythixLogger;
|
|
116
|
+
if (!logger)
|
|
117
|
+
logger = request.mythixLogger = this.createRequestLogger(application, request);
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
if (!request.Sequelize)
|
|
120
|
+
request.Sequelize = Sequelize;
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
var middlewareIndex = 0;
|
|
123
|
+
const next = async () => {
|
|
124
|
+
if (middlewareIndex >= middleware.length)
|
|
125
|
+
return resolve();
|
|
121
126
|
|
|
122
|
-
|
|
127
|
+
var middlewareFunc = middleware[middlewareIndex++];
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
try {
|
|
130
|
+
await middlewareFunc.call(this, request, response, next);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
var statusCode = error.statusCode || error.status_code || 500;
|
|
128
133
|
|
|
129
|
-
|
|
130
|
-
logger.log(`Error: ${statusCode} ${statusCodeToMessage(statusCode)}`);
|
|
131
|
-
this.errorHandler(error.getMessage(), statusCode, response, request);
|
|
132
|
-
} else {
|
|
133
|
-
if (statusCode) {
|
|
134
|
+
if (error instanceof HTTPBaseError) {
|
|
134
135
|
logger.log(`Error: ${statusCode} ${statusCodeToMessage(statusCode)}`);
|
|
135
|
-
this.errorHandler(error.
|
|
136
|
+
this.errorHandler(error.getMessage(), statusCode, response, request);
|
|
136
137
|
} else {
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
if (statusCode) {
|
|
139
|
+
logger.log(`Error: ${statusCode} ${statusCodeToMessage(statusCode)}`);
|
|
140
|
+
this.errorHandler(error.message, statusCode, response, request);
|
|
141
|
+
} else {
|
|
142
|
+
logger.log(`Error: ${error.message}`, error);
|
|
143
|
+
this.errorHandler(error.message, 500, response, request);
|
|
144
|
+
}
|
|
139
145
|
}
|
|
146
|
+
|
|
147
|
+
reject(error);
|
|
140
148
|
}
|
|
141
|
-
}
|
|
142
|
-
};
|
|
149
|
+
};
|
|
143
150
|
|
|
144
|
-
|
|
151
|
+
next().catch(reject);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
baseMiddleware(request, response, rootNext) {
|
|
156
|
+
var middleware = this.middleware;
|
|
157
|
+
if (Nife.isEmpty(middleware))
|
|
158
|
+
return rootNext();
|
|
159
|
+
|
|
160
|
+
this.executeMiddleware(middleware, request, response).then(
|
|
161
|
+
() => rootNext(),
|
|
162
|
+
(error) => {
|
|
163
|
+
if (!(error instanceof HTTPBaseError))
|
|
164
|
+
this.getApplication().error('Error in middleware: ', error);
|
|
165
|
+
}
|
|
166
|
+
);
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
findFirstMatchingRoute(request, _routes) {
|
|
@@ -160,7 +182,7 @@ class HTTPServer {
|
|
|
160
182
|
return;
|
|
161
183
|
|
|
162
184
|
if (typeof contentTypeMatcher === 'function' && !contentTypeMatcher(contentType))
|
|
163
|
-
throw new
|
|
185
|
+
throw new HTTPBadContentTypeError(route);
|
|
164
186
|
|
|
165
187
|
return result;
|
|
166
188
|
};
|
|
@@ -168,7 +190,7 @@ class HTTPServer {
|
|
|
168
190
|
var routes = _routes || [];
|
|
169
191
|
var method = request.method;
|
|
170
192
|
var contentType = Nife.get(request, 'headers.content-type');
|
|
171
|
-
var path = request.
|
|
193
|
+
var path = request.path;
|
|
172
194
|
|
|
173
195
|
for (var i = 0, il = routes.length; i < il; i++) {
|
|
174
196
|
var route = routes[i];
|
|
@@ -207,26 +229,81 @@ class HTTPServer {
|
|
|
207
229
|
|
|
208
230
|
var logger = application.getLogger();
|
|
209
231
|
var loggerMethod = ('' + request.method).toUpperCase();
|
|
210
|
-
var loggerURL = ('' + request.
|
|
232
|
+
var loggerURL = ('' + request.path);
|
|
211
233
|
var requestID = (Date.now() + Math.random()).toFixed(4);
|
|
212
234
|
var ipAddress = Nife.get(request, 'client.remoteAddress', '<unknown IP address>');
|
|
213
235
|
|
|
214
236
|
return logger.clone({ formatter: (output) => `{${ipAddress}} - [#${requestID} ${loggerMethod} ${loggerURL}]: ${output}`});
|
|
215
237
|
}
|
|
216
238
|
|
|
217
|
-
|
|
239
|
+
validateQueryParam(route, query, paramName, queryValue, queryParams) {
|
|
240
|
+
var { validate } = queryParams;
|
|
241
|
+
if (!validate)
|
|
242
|
+
return true;
|
|
243
|
+
|
|
244
|
+
if (validate instanceof RegExp)
|
|
245
|
+
return !!('' + queryValue).match(validate);
|
|
246
|
+
else if (typeof validate === 'function')
|
|
247
|
+
return !!validate.call(route, queryValue, paramName, query, queryParams);
|
|
248
|
+
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
compileQueryParams(route, query, queryParams) {
|
|
253
|
+
var finalQuery = Object.assign({}, query || {});
|
|
254
|
+
|
|
255
|
+
var paramNames = Object.keys(queryParams || {});
|
|
256
|
+
for (var i = 0, il = paramNames.length; i < il; i++) {
|
|
257
|
+
var paramName = paramNames[i];
|
|
258
|
+
var queryParam = queryParams[paramName];
|
|
259
|
+
if (!queryParam)
|
|
260
|
+
continue;
|
|
261
|
+
|
|
262
|
+
var queryValue = finalQuery[paramName];
|
|
263
|
+
if (queryValue == null) {
|
|
264
|
+
if (queryParam.required)
|
|
265
|
+
throw new HTTPBadRequestError(route, `Query param "${paramName}" is required`);
|
|
266
|
+
|
|
267
|
+
if (queryParam.hasOwnProperty('defaultValue'))
|
|
268
|
+
finalQuery[paramName] = queryParam['defaultValue'];
|
|
269
|
+
} else {
|
|
270
|
+
if (!this.validateQueryParam(route, finalQuery, paramName, queryValue, queryParam))
|
|
271
|
+
throw new HTTPBadRequestError(route, `Query param "${paramName}" is invalid`);
|
|
272
|
+
|
|
273
|
+
if (queryParam.hasOwnProperty('type'))
|
|
274
|
+
finalQuery[paramName] = Nife.coerceValue(queryValue, queryParam['type']);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return finalQuery;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async sendRequestToController(...args) {
|
|
218
282
|
var context = args[2];
|
|
219
283
|
var controllerInstance = context.controllerInstance;
|
|
220
284
|
|
|
221
|
-
|
|
285
|
+
// Compile query params
|
|
286
|
+
context.query = this.compileQueryParams(context.route, context.query, (context.route && context.route.queryParams));
|
|
287
|
+
|
|
288
|
+
var route = context.route;
|
|
289
|
+
|
|
290
|
+
// Execute middleware if any exists
|
|
291
|
+
var middleware = (typeof controllerInstance.getMiddleware === 'function') ? controllerInstance.getMiddleware.call(controllerInstance, context) : [];
|
|
292
|
+
if (route && Nife.instanceOf(route.middleware, 'array') && Nife.isNotEmpty(route.middleware))
|
|
293
|
+
middleware = route.middleware.concat((middleware) ? middleware : []);
|
|
294
|
+
|
|
295
|
+
if (Nife.isNotEmpty(middleware))
|
|
296
|
+
await this.executeMiddleware(middleware, request, response);
|
|
297
|
+
|
|
298
|
+
return await controllerInstance.handleIncomingRequest.apply(controllerInstance, args);
|
|
222
299
|
}
|
|
223
300
|
|
|
224
301
|
async baseRouter(request, response, next) {
|
|
225
|
-
var startTime
|
|
302
|
+
var startTime = Nife.now();
|
|
303
|
+
var application = this.getApplication();
|
|
226
304
|
|
|
227
305
|
try {
|
|
228
|
-
var
|
|
229
|
-
var logger = this.createRequestLogger(application, request, { controller, controllerMethod });
|
|
306
|
+
var logger = this.createRequestLogger(application, request, { controller, controllerMethod });
|
|
230
307
|
|
|
231
308
|
logger.log(`Starting request`);
|
|
232
309
|
|
|
@@ -250,6 +327,7 @@ class HTTPServer {
|
|
|
250
327
|
|
|
251
328
|
var context = {
|
|
252
329
|
params: request.params,
|
|
330
|
+
query: request.query,
|
|
253
331
|
route,
|
|
254
332
|
controller,
|
|
255
333
|
controllerMethod,
|
|
@@ -259,13 +337,18 @@ class HTTPServer {
|
|
|
259
337
|
|
|
260
338
|
var controllerResult = await this.sendRequestToController(request, response, context);
|
|
261
339
|
|
|
262
|
-
if (!response.finished)
|
|
340
|
+
if (!(response.finished || response.statusMessage))
|
|
263
341
|
await controllerInstance.handleOutgoingResponse(controllerResult, request, response, context);
|
|
342
|
+
else if (!response.finished)
|
|
343
|
+
response.end();
|
|
264
344
|
|
|
265
345
|
var statusCode = response.statusCode || 200;
|
|
266
346
|
var requestTime = Nife.now() - startTime;
|
|
267
347
|
logger.log(`Completed request in ${requestTime.toFixed(3)}ms: ${statusCode} ${response.statusMessage || statusCodeToMessage(statusCode)}`);
|
|
268
348
|
} catch (error) {
|
|
349
|
+
if ((error instanceof HTTPInternalServerError || !(error instanceof HTTPBaseError)) && application.getOptions().testMode)
|
|
350
|
+
console.error(error);
|
|
351
|
+
|
|
269
352
|
try {
|
|
270
353
|
var statusCode = error.statusCode || error.status_code || 500;
|
|
271
354
|
var requestTime = Nife.now() - startTime;
|
|
@@ -305,14 +388,15 @@ class HTTPServer {
|
|
|
305
388
|
}
|
|
306
389
|
|
|
307
390
|
async start() {
|
|
308
|
-
var options
|
|
309
|
-
var app
|
|
391
|
+
var options = this.getOptions();
|
|
392
|
+
var app = this.createExpressApplication(options);
|
|
393
|
+
var portString = (options.port) ? `:${options.port}` : '';
|
|
310
394
|
var server;
|
|
311
395
|
|
|
312
396
|
app.use(this.baseMiddleware.bind(this));
|
|
313
397
|
app.all('*', this.baseRouter.bind(this));
|
|
314
398
|
|
|
315
|
-
this.getLogger().log(`Starting ${(options.https) ? 'HTTPS' : 'HTTP'} server ${(options.https) ? 'https' : 'http'}://${options.host}
|
|
399
|
+
this.getLogger().log(`Starting ${(options.https) ? 'HTTPS' : 'HTTP'} server ${(options.https) ? 'https' : 'http'}://${options.host}${portString}...`);
|
|
316
400
|
|
|
317
401
|
if (options.https) {
|
|
318
402
|
var credentials = await this.getHTTPSCredentials(options.https);
|
|
@@ -325,7 +409,8 @@ class HTTPServer {
|
|
|
325
409
|
|
|
326
410
|
this.server = server;
|
|
327
411
|
|
|
328
|
-
|
|
412
|
+
var listeningPort = server.address().port;
|
|
413
|
+
this.getLogger().log(`Web server listening at ${(options.https) ? 'https' : 'http'}://${options.host}:${listeningPort}`);
|
|
329
414
|
|
|
330
415
|
return server;
|
|
331
416
|
}
|