mythix 2.8.7 → 2.8.9

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": "mythix",
3
- "version": "2.8.7",
3
+ "version": "2.8.9",
4
4
  "description": "Mythix is a NodeJS web-app framework",
5
5
  "main": "src/index",
6
6
  "scripts": {
@@ -20,17 +20,17 @@
20
20
  "homepage": "https://github.com/th317erd/mythix#readme",
21
21
  "devDependencies": {
22
22
  "@spothero/eslint-plugin-spothero": "github:spothero/eslint-plugin-spothero",
23
- "@types/node": "^18.7.17",
23
+ "@types/node": "^18.11.7",
24
24
  "colors": "^1.4.0",
25
25
  "diff": "^5.1.0",
26
- "eslint": "^8.23.1",
26
+ "eslint": "^8.26.0",
27
27
  "jasmine": "^4.4.0"
28
28
  },
29
29
  "dependencies": {
30
30
  "@types/events": "^3.0.0",
31
31
  "chokidar": "^3.5.3",
32
32
  "cmded": "^1.2.5",
33
- "express": "^4.18.1",
33
+ "express": "^4.18.2",
34
34
  "express-busboy": "github:th317erd/express-busboy#0754a570d7979097b31e48655b80d3fcd628d4e4",
35
35
  "form-data": "^4.0.0",
36
36
  "luxon": "^3.0.4",
@@ -106,19 +106,28 @@ function nodeRequestHandler(routeName, requestOptions) {
106
106
  });
107
107
 
108
108
  response.on('end', function() {
109
- response.rawBody = response.body = responseData;
109
+ response.rawBody = responseData;
110
+
111
+ if (response.statusCode > 399) {
112
+ var error = new Error(response.statusText);
113
+ error.response = response;
114
+
115
+ reject(error);
116
+ return;
117
+ }
110
118
 
111
119
  try {
112
120
  var contentType = response.headers['content-type'];
121
+ var data;
113
122
 
114
- if (contentType && contentType.match(/application\/json/i)) {
115
- var data = JSON.parse(responseData.toString('utf8'));
116
- response.body = data;
117
- } else if (contentType && contentType.match(/text\/(plain|html)/)) {
118
- response.body = responseData.toString('utf8');
119
- }
123
+ if (contentType && contentType.match(/application\/json/i))
124
+ data = JSON.parse(responseData.toString('utf8'));
125
+ else if (contentType && contentType.match(/text\/(plain|html)/))
126
+ data = responseData.toString('utf8');
127
+ else
128
+ data = response.body;
120
129
 
121
- resolve(response);
130
+ resolve({ response, body: data });
122
131
  } catch (error) {
123
132
  return reject(error);
124
133
  }
@@ -190,7 +199,7 @@ function browserRequestHandler(routeName, requestOptions) {
190
199
  if (typeof requestOptions.responseHandler === 'function')
191
200
  return requestOptions.responseHandler(response);
192
201
 
193
- if (!response.ok) {
202
+ if (!response.ok || response.statusCode > 399) {
194
203
  var error = new Error(response.statusText);
195
204
  error.response = response;
196
205
 
@@ -199,14 +208,16 @@ function browserRequestHandler(routeName, requestOptions) {
199
208
  }
200
209
 
201
210
  var contentType = response.headers.get('Content-Type');
202
- if (contentType && contentType.match(/application\/json/i)) {
203
- var data = response.json();
204
- response.body = data;
205
- } else if (contentType && contentType.match(/text\/(plain|html)/i)) {
206
- response.body = response.text();
207
- }
211
+ let data;
212
+
213
+ if (contentType && contentType.match(/application\/json/i))
214
+ data = response.json();
215
+ else if (contentType && contentType.match(/text\/(plain|html)/i))
216
+ data = response.text();
217
+ else
218
+ data = response.body;
208
219
 
209
- resolve(response);
220
+ resolve({ response, body: data });
210
221
  },
211
222
  function(error) {
212
223
  reject(error);
@@ -0,0 +1,528 @@
1
+ 'use strict';
2
+
3
+ /* global Buffer */
4
+
5
+ const Path = require('path');
6
+ const FileSystem = require('fs');
7
+ const HTTP = require('http');
8
+ const HTTPS = require('https');
9
+ const Nife = require('nife');
10
+ const Express = require('express');
11
+ const ExpressBusBoy = require('express-busboy');
12
+
13
+ const {
14
+ HTTPBaseError,
15
+ HTTPNotFoundError,
16
+ HTTPBadRequestError,
17
+ HTTPBadContentTypeError,
18
+ HTTPInternalServerError,
19
+ } = require('./http-errors');
20
+
21
+ const {
22
+ statusCodeToMessage,
23
+ } = require('../utils/http-utils');
24
+
25
+ const REQUEST_ID_POSTFIX_LENGTH = 4;
26
+ const REQUEST_TIME_RESOLUTION = 3;
27
+
28
+ const DEFAULT_FILE_UPLOAD_BUFFER_SIZE = 2 * 1024 * 1024; // 2mb
29
+ const DEFAULT_FILE_UPLOAD_SIZE_LIMIT = 2 * 1024 * 1024; // 10mb
30
+
31
+ class HTTPServer {
32
+ constructor(application, _opts) {
33
+ let uploadPath = Path.resolve(application.getTempPath(), 'uploads');
34
+
35
+ let opts = Nife.extend(true, {
36
+ host: 'localhost',
37
+ port: '8000',
38
+ https: false,
39
+ uploads: {
40
+ upload: true,
41
+ path: uploadPath,
42
+ allowedPath: /./i,
43
+ highWaterMark: DEFAULT_FILE_UPLOAD_BUFFER_SIZE,
44
+ limits: {
45
+ fileSize: DEFAULT_FILE_UPLOAD_SIZE_LIMIT,
46
+ },
47
+ },
48
+ }, _opts || {});
49
+
50
+ Object.defineProperties(this, {
51
+ 'application': {
52
+ writable: false,
53
+ enumerable: false,
54
+ configurable: true,
55
+ value: application,
56
+ },
57
+ 'server': {
58
+ writable: true,
59
+ enumerable: false,
60
+ configurable: true,
61
+ value: null,
62
+ },
63
+ 'options': {
64
+ writable: false,
65
+ enumerable: false,
66
+ configurable: true,
67
+ value: opts,
68
+ },
69
+ 'routes': {
70
+ writable: true,
71
+ enumerable: false,
72
+ configurable: true,
73
+ value: null,
74
+ },
75
+ 'middleware': {
76
+ writable: true,
77
+ enumerable: false,
78
+ configurable: true,
79
+ value: opts.middleware,
80
+ },
81
+ });
82
+ }
83
+
84
+ getApplication() {
85
+ return this.application;
86
+ }
87
+
88
+ getLogger() {
89
+ let application = this.getApplication();
90
+ return application.getLogger();
91
+ }
92
+
93
+ getOptions() {
94
+ return this.options;
95
+ }
96
+
97
+ getHTTPSCredentials(options) {
98
+ let keyContent = options.key;
99
+ if (!keyContent && options.keyPath)
100
+ keyContent = FileSystem.readFileSync(options.keyPath, 'latin1');
101
+
102
+ let certContent = options.cert;
103
+ if (!certContent && options.certPath)
104
+ certContent = FileSystem.readFileSync(options.certPath, 'latin1');
105
+
106
+ return {
107
+ key: keyContent,
108
+ cert: certContent,
109
+ };
110
+ }
111
+
112
+ setRoutes(routes) {
113
+ this.routes = routes;
114
+ }
115
+
116
+ executeMiddleware(middleware, request, response) {
117
+ let { route, params } = (this.findFirstMatchingRoute(request, this.routes) || {});
118
+
119
+ return new Promise((resolve, reject) => {
120
+ if (Nife.isEmpty(middleware)) {
121
+ resolve();
122
+ return;
123
+ }
124
+
125
+ let application = this.getApplication();
126
+ if (!request.mythixApplication)
127
+ request.mythixApplication = application;
128
+
129
+ let logger = request.mythixLogger;
130
+ if (!logger)
131
+ logger = request.mythixLogger = this.createRequestLogger(application, request);
132
+
133
+ request.route = route;
134
+ request.params = params;
135
+
136
+ let middlewareIndex = 0;
137
+ const next = async () => {
138
+ if (middlewareIndex >= middleware.length)
139
+ return resolve();
140
+
141
+ let middlewareFunc = middleware[middlewareIndex++];
142
+
143
+ try {
144
+ await middlewareFunc.call(this, request, response, next);
145
+ } catch (error) {
146
+ let statusCode = error.statusCode || error.status_code || 500;
147
+
148
+ if (error instanceof HTTPBaseError) {
149
+ logger.error(`Error: ${statusCode} ${statusCodeToMessage(statusCode)}`);
150
+ this.errorHandler(error, error.getMessage(), statusCode, response, request);
151
+ } else {
152
+ if (statusCode) {
153
+ logger.error(`Error: ${statusCode} ${statusCodeToMessage(statusCode)}`);
154
+ this.errorHandler(error, error.message, statusCode, response, request);
155
+ } else {
156
+ logger.error(`Error: ${error.message}`, error);
157
+ this.errorHandler(error, error.message, 500, response, request);
158
+ }
159
+ }
160
+
161
+ reject(error);
162
+ }
163
+ };
164
+
165
+ next().catch(reject);
166
+ });
167
+ }
168
+
169
+ baseMiddleware(request, response, rootNext) {
170
+ let middleware = this.middleware;
171
+ if (Nife.isEmpty(middleware))
172
+ return rootNext();
173
+
174
+ this.executeMiddleware(middleware, request, response).then(
175
+ () => rootNext(),
176
+ (error) => {
177
+ if (!(error instanceof HTTPBaseError))
178
+ this.getApplication().getLogger().error('Error in middleware: ', error);
179
+ },
180
+ );
181
+ }
182
+
183
+ findFirstMatchingRoute(request, _routes) {
184
+ const routeMatcher = (route, method, path, contentType) => {
185
+ let {
186
+ methodMatcher,
187
+ contentTypeMatcher,
188
+ pathMatcher,
189
+ } = route;
190
+
191
+ if (typeof methodMatcher === 'function' && !methodMatcher(method))
192
+ return;
193
+
194
+ let result = (typeof pathMatcher !== 'function') ? false : pathMatcher(path);
195
+ if (!result)
196
+ return;
197
+
198
+ if (typeof contentTypeMatcher === 'function' && !contentTypeMatcher(contentType))
199
+ throw new HTTPBadContentTypeError(route);
200
+
201
+ return result;
202
+ };
203
+
204
+ let routes = _routes || [];
205
+ let method = request.method;
206
+ let contentType = Nife.get(request, 'headers.content-type');
207
+ let path = request.path;
208
+
209
+ for (let i = 0, il = routes.length; i < il; i++) {
210
+ let route = routes[i];
211
+ let result = routeMatcher(route, method, path, contentType);
212
+ if (!result)
213
+ continue;
214
+
215
+ return { route, params: result };
216
+ }
217
+
218
+ throw new HTTPNotFoundError();
219
+ }
220
+
221
+ getRouteController(_controller, route, params, request) {
222
+ let controller = _controller;
223
+
224
+ if (typeof controller === 'function') {
225
+ if (controller.constructor === Function.prototype.constructor) {
226
+ controller = controller.call(this, request, route, params);
227
+ if (Nife.instanceOf(controller, 'string'))
228
+ controller = this.getApplication().getController(controller);
229
+ else if (typeof controller === 'function')
230
+ controller = { controller };
231
+ } else {
232
+ controller = { controller };
233
+ }
234
+ } else if (Nife.instanceOf(controller, 'string')) {
235
+ controller = this.getApplication().getController(controller);
236
+ }
237
+
238
+ return controller;
239
+ }
240
+
241
+ createRequestLogger(application, request) {
242
+ let requestID = (Date.now() + Math.random()).toFixed(REQUEST_ID_POSTFIX_LENGTH);
243
+
244
+ if (request.mythixLogger) {
245
+ if (!request.mythixRequestID)
246
+ request.mythixRequestID = requestID;
247
+
248
+ return request.mythixLogger;
249
+ }
250
+
251
+ let logger = application.getLogger();
252
+ let loggerMethod = ('' + request.method).toUpperCase();
253
+ let loggerURL = ('' + request.path);
254
+ let ipAddress = Nife.get(request, 'client.remoteAddress', '<unknown IP address>');
255
+
256
+ request.mythixRequestID = requestID;
257
+
258
+ return logger.clone({ formatter: (output) => `{${ipAddress}} - [#${requestID} ${loggerMethod} ${loggerURL}]: ${output}`});
259
+ }
260
+
261
+ validateQueryParam(route, query, paramName, queryValue, queryParams) {
262
+ let { validate } = queryParams;
263
+ if (!validate)
264
+ return true;
265
+
266
+ if (validate instanceof RegExp)
267
+ return !!('' + queryValue).match(validate);
268
+ else if (typeof validate === 'function')
269
+ return !!validate.call(route, queryValue, paramName, query, queryParams);
270
+
271
+ return true;
272
+ }
273
+
274
+ compileQueryParams(route, query, queryParams) {
275
+ let finalQuery = Object.assign({}, query || {});
276
+
277
+ let paramNames = Object.keys(queryParams || {});
278
+ for (let i = 0, il = paramNames.length; i < il; i++) {
279
+ let paramName = paramNames[i];
280
+ let queryParam = queryParams[paramName];
281
+ if (!queryParam)
282
+ continue;
283
+
284
+ let queryValue = finalQuery[paramName];
285
+ if (queryValue == null) {
286
+ if (queryParam.required)
287
+ throw new HTTPBadRequestError(route, `Query param "${paramName}" is required`);
288
+
289
+ if (Object.prototype.hasOwnProperty.call(queryParam, 'defaultValue'))
290
+ finalQuery[paramName] = queryParam['defaultValue'];
291
+ } else {
292
+ if (!this.validateQueryParam(route, finalQuery, paramName, queryValue, queryParam))
293
+ throw new HTTPBadRequestError(route, `Query param "${paramName}" is invalid`);
294
+
295
+ if (Object.prototype.hasOwnProperty.call(queryParam, 'type'))
296
+ finalQuery[paramName] = Nife.coerceValue(queryValue, queryParam['type']);
297
+ }
298
+ }
299
+
300
+ return finalQuery;
301
+ }
302
+
303
+ async sendRequestToController(request, response, context) {
304
+ const executeRequest = async () => {
305
+ let controllerInstance = context.controllerInstance;
306
+
307
+ // Compile query params
308
+ context.query = this.compileQueryParams(context.route, context.query, (context.route && context.route.queryParams));
309
+
310
+ let route = context.route;
311
+
312
+ // Execute middleware if any exists
313
+ let middleware = (typeof controllerInstance.getMiddleware === 'function') ? controllerInstance.getMiddleware.call(controllerInstance, context) : [];
314
+ if (route && Nife.instanceOf(route.middleware, 'array') && Nife.isNotEmpty(route.middleware))
315
+ middleware = route.middleware.concat((middleware) ? middleware : []);
316
+
317
+ if (Nife.isNotEmpty(middleware))
318
+ await this.executeMiddleware(middleware, request, response);
319
+
320
+ return await controllerInstance.handleIncomingRequest.apply(controllerInstance, [ request, response, context ]);
321
+ };
322
+
323
+ let application = this.getApplication();
324
+ let dbConnection = (typeof application.getDBConnection === 'function') ? application.getDBConnection() : undefined;
325
+
326
+ if (dbConnection && typeof dbConnection.createContext === 'function')
327
+ return await dbConnection.createContext(executeRequest, dbConnection, dbConnection);
328
+ else
329
+ return await executeRequest();
330
+ }
331
+
332
+ async baseRouter(request, response, next) {
333
+ let startTime = Nife.now();
334
+ let application = this.getApplication();
335
+ let controllerInstance;
336
+ let logger;
337
+
338
+ try {
339
+ logger = this.createRequestLogger(application, request);
340
+ logger.info('Starting request');
341
+
342
+ let { route, params } = (this.findFirstMatchingRoute(request, this.routes) || {});
343
+
344
+ request.params = params || {};
345
+
346
+ let _controller = this.getRouteController(route.controller, route, params, request);
347
+ let {
348
+ controller,
349
+ controllerMethod,
350
+ } = (_controller || {});
351
+
352
+ let ControllerConstructor = controller;
353
+
354
+ if (!controller)
355
+ throw new HTTPInternalServerError(route, `Controller not found for route ${route.url}`);
356
+
357
+ if (Nife.isEmpty(controllerMethod))
358
+ controllerMethod = (request.method || 'get').toLowerCase();
359
+
360
+ controllerInstance = new ControllerConstructor(application, logger || application.getLogger(), request, response);
361
+
362
+ let context = {
363
+ params: request.params,
364
+ query: request.query,
365
+ route,
366
+ controller,
367
+ controllerMethod,
368
+ controllerInstance,
369
+ startTime,
370
+ };
371
+
372
+ let controllerResult = await this.sendRequestToController(request, response, context);
373
+
374
+ if (!(response.finished || response.statusMessage)) {
375
+ const handleOutgoing = async () => {
376
+ return await controllerInstance.handleOutgoingResponse(controllerResult, request, response, context);
377
+ };
378
+
379
+ let dbConnection = (typeof application.getDBConnection === 'function') ? application.getDBConnection() : undefined;
380
+ if (dbConnection && typeof dbConnection.createContext === 'function')
381
+ await dbConnection.createContext(handleOutgoing, dbConnection, dbConnection);
382
+ else
383
+ await handleOutgoing();
384
+ } else if (!response.finished) {
385
+ response.end();
386
+ }
387
+
388
+ let statusCode = response.statusCode || 200;
389
+ let requestTime = Nife.now() - startTime;
390
+
391
+ logger.log(`Completed request in ${requestTime.toFixed(REQUEST_TIME_RESOLUTION)}ms: ${statusCode} ${response.statusMessage || statusCodeToMessage(statusCode)}`);
392
+ } catch (error) {
393
+ if ((error instanceof HTTPInternalServerError || !(error instanceof HTTPBaseError)) && application.getOptions().testMode)
394
+ (logger || application.getLogger()).error(error);
395
+
396
+ let requestTime = Nife.now() - startTime;
397
+ let statusCode;
398
+
399
+ try {
400
+ statusCode = error.statusCode || error.status_code || 500;
401
+
402
+ if (controllerInstance && typeof controllerInstance.errorHandler === 'function')
403
+ await controllerInstance.errorHandler(error, statusCode, request, response);
404
+ else if (error instanceof HTTPBaseError)
405
+ await this.errorHandler(error, error.getMessage(), statusCode, response, request);
406
+ else
407
+ await this.errorHandler(error, error.message, statusCode, response, request);
408
+
409
+ } catch (error2) {
410
+ statusCode = error2.statusCode || error2.status_code || 500;
411
+
412
+ await this.errorHandler(error2, error2.message, statusCode, response, request);
413
+
414
+ logger.log(`Completed request in ${requestTime.toFixed(REQUEST_TIME_RESOLUTION)}ms: ${statusCode} ${statusCodeToMessage(statusCode)}`, error2);
415
+
416
+ return;
417
+ }
418
+
419
+ (logger || application.getLogger()).log(`Completed request in ${requestTime.toFixed(REQUEST_TIME_RESOLUTION)}ms: ${statusCode} ${statusCodeToMessage(statusCode)}`, error);
420
+ }
421
+
422
+ return next();
423
+ }
424
+
425
+ errorHandler(error, message, statusCode, response /*, request */) {
426
+ if (response.statusMessage)
427
+ return;
428
+
429
+ if (error && error.headers) {
430
+ let headers = error.headers;
431
+ let headerKeys = Object.keys(headers);
432
+
433
+ for (let i = 0, il = headerKeys.length; i < il; i++) {
434
+ let headerKey = headerKeys[i];
435
+ let value = headers[headerKey];
436
+ if (value == null)
437
+ continue;
438
+
439
+ response.header(headerKey, ('' + value));
440
+ }
441
+ }
442
+
443
+ response.status(statusCode || 500).send(message || statusCodeToMessage(statusCode) || 'Internal Server Error');
444
+ }
445
+
446
+ createExpressApplication(options) {
447
+ // eslint-disable-next-line new-cap
448
+ let app = Express();
449
+
450
+ app.use(Express.raw({ type: 'application/json' }));
451
+
452
+ // Store _rawBody for request
453
+ app.use((request, response, next) => {
454
+ if ((/application\/json/i).test(request.headers['content-type']) && Buffer.isBuffer(request.body)) {
455
+ let bodyStr = request.body.toString('utf8');
456
+ request._rawBody = bodyStr;
457
+
458
+ try {
459
+ request.body = JSON.parse(bodyStr);
460
+ } catch (error) {
461
+ request.body = bodyStr;
462
+ }
463
+ }
464
+
465
+ next();
466
+ });
467
+
468
+ ExpressBusBoy.extend(app, options.uploads);
469
+
470
+ return app;
471
+ }
472
+
473
+ async start() {
474
+ let options = this.getOptions();
475
+ let app = this.createExpressApplication(options);
476
+ let portString = (options.port) ? `:${options.port}` : '';
477
+ let server;
478
+
479
+ app.use(this.baseMiddleware.bind(this));
480
+ app.all('*', this.baseRouter.bind(this));
481
+
482
+ this.getLogger().log(`Starting ${(options.https) ? 'HTTPS' : 'HTTP'} server ${(options.https) ? 'https' : 'http'}://${options.host}${portString}...`);
483
+
484
+ if (options.https) {
485
+ let credentials = await this.getHTTPSCredentials(options.https);
486
+ server = HTTPS.createServer(credentials, app);
487
+ } else {
488
+ server = HTTP.createServer(app);
489
+ }
490
+
491
+ server.listen(options.port);
492
+
493
+ this.server = server;
494
+
495
+ let listeningPort = server.address().port;
496
+
497
+ this.getLogger().info(`Web server listening at ${(options.https) ? 'https' : 'http'}://${options.host}:${listeningPort}`);
498
+
499
+ return server;
500
+ }
501
+
502
+ async stop() {
503
+ let server = this.server;
504
+ if (!server)
505
+ return;
506
+
507
+ try {
508
+ this.getLogger().info('Shutting down web server...');
509
+
510
+ await new Promise((resolve, reject) => {
511
+ server.close((error) => {
512
+ if (error)
513
+ return reject(error);
514
+
515
+ resolve();
516
+ });
517
+ });
518
+
519
+ this.getLogger().info('Web server shut down successfully!');
520
+ } catch (error) {
521
+ this.getLogger().error('Error stopping HTTP server: ', error);
522
+ }
523
+ }
524
+ }
525
+
526
+ module.exports = {
527
+ HTTPServer,
528
+ };