mythix 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 +4 -3
- package/spec/controller-utils-spec.js +1 -1
- package/src/application.js +23 -7
- package/src/cli/cli-utils.js +5 -0
- package/src/cli/migrations/migrate-command.js +1 -1
- 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 +48 -10
- 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.3",
|
|
4
4
|
"description": "Mythix is a NodeJS web-app framework",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
"express-busboy": "^8.0.0",
|
|
28
28
|
"inflection": "^1.13.1",
|
|
29
29
|
"lodash": "^4.17.21",
|
|
30
|
-
"nife": "^1.
|
|
30
|
+
"nife": "^1.5.0",
|
|
31
31
|
"object-hash": "^2.2.0",
|
|
32
|
-
"sequelize": "^6.13.0"
|
|
32
|
+
"sequelize": "^6.13.0",
|
|
33
|
+
"sqlite3": "^5.0.2"
|
|
33
34
|
}
|
|
34
35
|
}
|
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,12 +778,17 @@ 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) {
|
|
@@ -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;
|
|
@@ -81,7 +81,7 @@ module.exports = defineCommand('migrate', ({ Parent }) => {
|
|
|
81
81
|
var useTransaction = (args['transaction']) ? true : false;
|
|
82
82
|
var rollback = args.rollback;
|
|
83
83
|
|
|
84
|
-
console.log('USING TRANSACTION: ', useTransaction, args['
|
|
84
|
+
console.log('USING TRANSACTION: ', useTransaction, args['transaction']);
|
|
85
85
|
|
|
86
86
|
if (args.revision)
|
|
87
87
|
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
|
}
|
package/src/index.js
CHANGED
|
@@ -8,22 +8,25 @@ const { Logger } = require('./logger');
|
|
|
8
8
|
const Utils = require('./utils');
|
|
9
9
|
|
|
10
10
|
module.exports = {
|
|
11
|
-
defineCommand:
|
|
12
|
-
defineController:
|
|
13
|
-
defineModel:
|
|
14
|
-
defineTask:
|
|
11
|
+
defineCommand: CLIUtilsScope.defineCommand,
|
|
12
|
+
defineController: ControllerScope.defineController,
|
|
13
|
+
defineModel: Models.defineModel,
|
|
14
|
+
defineTask: TasksScope.defineTask,
|
|
15
|
+
createTestApplication: Utils.TestUtils.createTestApplication,
|
|
15
16
|
|
|
16
|
-
CLI:
|
|
17
|
-
ControllerBase:
|
|
18
|
-
Controllers:
|
|
19
|
-
CryptoUtils:
|
|
20
|
-
HTTP:
|
|
21
|
-
HTTPErrors:
|
|
22
|
-
HTTPServer:
|
|
23
|
-
HTTPUtils:
|
|
24
|
-
Middleware:
|
|
25
|
-
TaskBase:
|
|
26
|
-
Tasks:
|
|
17
|
+
CLI: CLIUtilsScope,
|
|
18
|
+
ControllerBase: ControllerScope.ControllerBase,
|
|
19
|
+
Controllers: ControllerScope,
|
|
20
|
+
CryptoUtils: Utils.CryptoUtils,
|
|
21
|
+
HTTP: HTTPServerScope,
|
|
22
|
+
HTTPErrors: HTTPServerScope.HTTPErrors,
|
|
23
|
+
HTTPServer: HTTPServerScope.HTTPServer,
|
|
24
|
+
HTTPUtils: Utils.HTTPUtils,
|
|
25
|
+
Middleware: HTTPServerScope.Middleware,
|
|
26
|
+
TaskBase: TasksScope.TaskBase,
|
|
27
|
+
Tasks: TasksScope,
|
|
28
|
+
TestUtils: Utils.TestUtils,
|
|
29
|
+
Model: Models.Model,
|
|
27
30
|
|
|
28
31
|
Application,
|
|
29
32
|
Logger,
|
package/src/logger.js
CHANGED
|
@@ -2,9 +2,10 @@ const Path = require('path');
|
|
|
2
2
|
const FileSystem = require('fs');
|
|
3
3
|
|
|
4
4
|
const LEVEL_ERROR = 1;
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
5
|
+
const LEVEL_LOG = 2;
|
|
6
|
+
const LEVEL_WARN = 3;
|
|
7
|
+
const LEVEL_INFO = 4;
|
|
8
|
+
const LEVEL_DEBUG = 5;
|
|
8
9
|
|
|
9
10
|
function errorStackToString(rootPath, error) {
|
|
10
11
|
return ('\n -> ' + error.stack.split(/\n+/).slice(1).map((part) => `${part.replace(/^\s+at\s+/, '')}\n`).join(' -> ')).trimEnd();
|
|
@@ -48,8 +49,11 @@ function logToWriter(type, ..._args) {
|
|
|
48
49
|
|
|
49
50
|
try {
|
|
50
51
|
arg = JSON.stringify(arg);
|
|
51
|
-
} catch (
|
|
52
|
-
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.message = 'Converting circular structure to JSON')
|
|
54
|
+
return '<circular>';
|
|
55
|
+
|
|
56
|
+
return `<LOGGER_ERROR: ${error.message}>`;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
return arg;
|
|
@@ -88,7 +92,7 @@ class Logger {
|
|
|
88
92
|
|
|
89
93
|
Object.defineProperties(this, {
|
|
90
94
|
'_level': {
|
|
91
|
-
writable:
|
|
95
|
+
writable: true,
|
|
92
96
|
enumerable: false,
|
|
93
97
|
configurable: false,
|
|
94
98
|
value: opts.level,
|
|
@@ -132,13 +136,18 @@ class Logger {
|
|
|
132
136
|
});
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
setLevel(level) {
|
|
140
|
+
this._level = level;
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
clone(extraOpts) {
|
|
136
144
|
return new this.constructor(Object.assign({
|
|
137
|
-
level:
|
|
138
|
-
writer:
|
|
139
|
-
pid:
|
|
140
|
-
formatter:
|
|
141
|
-
rootPath:
|
|
145
|
+
level: this._level,
|
|
146
|
+
writer: this._writer,
|
|
147
|
+
pid: this._pid,
|
|
148
|
+
formatter: this._formatter,
|
|
149
|
+
rootPath: this._rootPath,
|
|
150
|
+
errorStackFormatter: this._errorStackFormatter,
|
|
142
151
|
}, extraOpts || {}));
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -146,6 +155,10 @@ class Logger {
|
|
|
146
155
|
return (this._level >= LEVEL_ERROR);
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
isLogLevel() {
|
|
159
|
+
return (this._level >= LEVEL_LOG);
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
isWarningLevel() {
|
|
150
163
|
return (this._level >= LEVEL_WARN);
|
|
151
164
|
}
|
|
@@ -179,7 +192,7 @@ class Logger {
|
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
log(...args) {
|
|
182
|
-
if (this.
|
|
195
|
+
if (this.isLogLevel())
|
|
183
196
|
logToWriter.call(this, 'log', ...args);
|
|
184
197
|
}
|
|
185
198
|
|
|
@@ -201,6 +214,7 @@ class Logger {
|
|
|
201
214
|
|
|
202
215
|
Object.assign(Logger, {
|
|
203
216
|
ERROR: LEVEL_ERROR,
|
|
217
|
+
LOG: LEVEL_LOG,
|
|
204
218
|
WARN: LEVEL_WARN,
|
|
205
219
|
INFO: LEVEL_INFO,
|
|
206
220
|
DEBUG: LEVEL_DEBUG,
|
|
@@ -34,14 +34,28 @@ function relationHelper(type) {
|
|
|
34
34
|
const RELATION_HELPERS = {
|
|
35
35
|
hasOne: relationHelper('hasOne'),
|
|
36
36
|
belongsTo: relationHelper('belongsTo'),
|
|
37
|
-
hasMany: relationHelper('
|
|
37
|
+
hasMany: relationHelper('hasMany'),
|
|
38
38
|
belongsToMany: relationHelper('belongsToMany'),
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
function preciseNow() {
|
|
42
|
+
var janFirst2022 = 1640995200000;
|
|
43
|
+
var now = Date.now() - janFirst2022;
|
|
44
|
+
var highResolutionNow = Nife.now();
|
|
45
|
+
var diff = Math.floor(highResolutionNow);
|
|
46
|
+
|
|
47
|
+
return Math.floor((now + (highResolutionNow - diff)) * 1000);
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
function defineModel(modelName, definer, _parent) {
|
|
42
|
-
function compileModelFields(Klass, DataTypes) {
|
|
51
|
+
function compileModelFields(Klass, DataTypes, application, connection) {
|
|
52
|
+
const createAutoIncrementor = () => {
|
|
53
|
+
return () => preciseNow();
|
|
54
|
+
};
|
|
55
|
+
|
|
43
56
|
var fields = Klass.fields;
|
|
44
57
|
var fieldNames = Object.keys(fields);
|
|
58
|
+
var isSQLIte = !!('' + Nife.get(connection, 'options.dialect')).match(/sqlite/);
|
|
45
59
|
|
|
46
60
|
for (var i = 0, il = fieldNames.length; i < il; i++) {
|
|
47
61
|
var fieldName = fieldNames[i];
|
|
@@ -52,6 +66,14 @@ function defineModel(modelName, definer, _parent) {
|
|
|
52
66
|
field.field = columnName;
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
// If using SQLite, which doesn't support autoincrement
|
|
70
|
+
// on anything except the primary key, then create our
|
|
71
|
+
// own auto-incrementor for this field
|
|
72
|
+
if (field.autoIncrement && isSQLIte && !field.primaryKey) {
|
|
73
|
+
application.getLogger().warn(`!Warning!: Using an auto-increment field in SQLite on a non-primary-key column "${field.field}"! Be aware that this functionality is now emulated using high resolution timestamps. This won't work unless the column is a BIGINT. You may run into serious problems with this emulation!`)
|
|
74
|
+
field.defaultValue = createAutoIncrementor();
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
if (field.type === DataTypes.BIGINT) {
|
|
56
78
|
if (!field.get) {
|
|
57
79
|
field.get = function(name) {
|
|
@@ -80,15 +102,26 @@ function defineModel(modelName, definer, _parent) {
|
|
|
80
102
|
return fields;
|
|
81
103
|
}
|
|
82
104
|
|
|
83
|
-
function cleanModelFields(Klass) {
|
|
105
|
+
function cleanModelFields(Klass, connection) {
|
|
84
106
|
var finalFields = {};
|
|
85
107
|
var fields = Klass.fields;
|
|
86
108
|
var fieldNames = Object.keys(fields);
|
|
109
|
+
var isSQLIte = !!('' + Nife.get(connection, 'options.dialect')).match(/sqlite/);
|
|
87
110
|
|
|
88
111
|
for (var i = 0, il = fieldNames.length; i < il; i++) {
|
|
89
112
|
var fieldName = fieldNames[i];
|
|
90
113
|
var field = fields[fieldName];
|
|
91
|
-
var newField = Nife.extend(Nife.extend.FILTER, (key) =>
|
|
114
|
+
var newField = Nife.extend(Nife.extend.FILTER, (key) => {
|
|
115
|
+
if (key.match(/^(index)$/))
|
|
116
|
+
return false;
|
|
117
|
+
|
|
118
|
+
// Strip "autoIncrement" if this is not the primary key
|
|
119
|
+
// and we are using sqlite for our dialect
|
|
120
|
+
if (key === 'autoIncrement' && isSQLIte && !field.primaryKey)
|
|
121
|
+
return false;
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}, {}, field);
|
|
92
125
|
|
|
93
126
|
finalFields[fieldName] = newField;
|
|
94
127
|
}
|
|
@@ -135,7 +168,7 @@ function defineModel(modelName, definer, _parent) {
|
|
|
135
168
|
}
|
|
136
169
|
|
|
137
170
|
return function({ application, Sequelize, connection }) {
|
|
138
|
-
var
|
|
171
|
+
var definerArgs = {
|
|
139
172
|
Parent: (_parent) ? _parent : Model,
|
|
140
173
|
Type: Sequelize.DataTypes,
|
|
141
174
|
Relation: RELATION_HELPERS,
|
|
@@ -143,19 +176,24 @@ function defineModel(modelName, definer, _parent) {
|
|
|
143
176
|
connection,
|
|
144
177
|
modelName,
|
|
145
178
|
application,
|
|
146
|
-
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
var Klass = definer(definerArgs);
|
|
147
182
|
|
|
148
183
|
Klass.name = modelName;
|
|
149
184
|
|
|
185
|
+
if (typeof Klass.onModelClassCreate === 'function')
|
|
186
|
+
Klass = Klass.onModelClassCreate(Klass, definerArgs);
|
|
187
|
+
|
|
150
188
|
var pluralName = (Klass.pluralName) ? Klass.pluralName : Inflection.pluralize(modelName);
|
|
151
189
|
if (Klass.pluralName !== pluralName)
|
|
152
190
|
Klass.pluralName = pluralName;
|
|
153
191
|
|
|
154
|
-
Klass.fields = compileModelFields(Klass, Sequelize.DataTypes);
|
|
192
|
+
Klass.fields = compileModelFields(Klass, Sequelize.DataTypes, application, connection);
|
|
155
193
|
|
|
156
194
|
var indexes = generateIndexes(Klass);
|
|
157
195
|
|
|
158
|
-
Klass.fields = cleanModelFields(Klass);
|
|
196
|
+
Klass.fields = cleanModelFields(Klass, connection);
|
|
159
197
|
|
|
160
198
|
var applicationOptions = application.getOptions();
|
|
161
199
|
var tableName;
|
|
@@ -177,8 +215,8 @@ function defineModel(modelName, definer, _parent) {
|
|
|
177
215
|
Klass.getPrimaryKeyField = getModelPrimaryKeyField.bind(this, Klass);
|
|
178
216
|
Klass.getPrimaryKeyFieldName = () => (getModelPrimaryKeyField(Klass).field);
|
|
179
217
|
|
|
180
|
-
if (typeof Klass.
|
|
181
|
-
Klass = Klass.
|
|
218
|
+
if (typeof Klass.onModelClassFinalized === 'function')
|
|
219
|
+
Klass = Klass.onModelClassFinalized(Klass, definerArgs);
|
|
182
220
|
|
|
183
221
|
return { [modelName]: Klass };
|
|
184
222
|
};
|
package/src/models/model.js
CHANGED
|
@@ -11,6 +11,11 @@ class Model extends Sequelize.Model {
|
|
|
11
11
|
return application.getLogger();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
getDBConnection() {
|
|
15
|
+
var application = this.getApplication();
|
|
16
|
+
return application.getDBConnection();
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
getPrimaryKeyField() {
|
|
15
20
|
return this.constructor.getPrimaryKeyField();
|
|
16
21
|
}
|
|
@@ -20,12 +25,16 @@ class Model extends Sequelize.Model {
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
static prepareWhereStatement(conditions) {
|
|
23
|
-
if (Nife.isEmpty(conditions))
|
|
28
|
+
if (Nife.isEmpty(conditions)) {
|
|
24
29
|
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (conditions._mythixQuery)
|
|
33
|
+
return conditions;
|
|
25
34
|
|
|
26
35
|
const Ops = Sequelize.Op;
|
|
27
36
|
var finalQuery = {};
|
|
28
|
-
var keys = Object.keys(conditions);
|
|
37
|
+
var keys = Object.keys(conditions).concat(Object.getOwnPropertySymbols(conditions));
|
|
29
38
|
|
|
30
39
|
for (var i = 0, il = keys.length; i < il; i++) {
|
|
31
40
|
var key = keys[i];
|
|
@@ -34,47 +43,134 @@ class Model extends Sequelize.Model {
|
|
|
34
43
|
if (value === undefined)
|
|
35
44
|
continue;
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
var name = key;
|
|
47
|
+
var invert = false;
|
|
48
|
+
|
|
49
|
+
if (typeof name === 'string' && name.charAt(0) === '!') {
|
|
50
|
+
name = name.substring(1);
|
|
51
|
+
invert = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof key === 'symbol') {
|
|
55
|
+
finalQuery[key] = value;
|
|
56
|
+
} else if (value === null) {
|
|
57
|
+
finalQuery[name] = (invert) ? { [Ops.not]: value } : { [Ops.is]: value };
|
|
39
58
|
} else if (Nife.instanceOf(value, 'number', 'string', 'boolean', 'bigint')) {
|
|
40
|
-
finalQuery[
|
|
59
|
+
finalQuery[name] = (invert) ? { [Ops.ne]: value } : { [Ops.eq]: value };
|
|
41
60
|
} else if (Nife.instanceOf(value, 'array') && Nife.isNotEmpty(value)) {
|
|
42
|
-
finalQuery[
|
|
61
|
+
finalQuery[name] = (invert) ? { [Ops.not]: { [Ops.in]: value } } : { [Ops.in]: value };
|
|
43
62
|
} else if (Nife.isNotEmpty(value)) {
|
|
44
|
-
|
|
63
|
+
if (invert)
|
|
64
|
+
throw new Error(`Model.prepareWhereStatement: Attempted to invert a custom matcher "${name}"`);
|
|
65
|
+
|
|
66
|
+
finalQuery[name] = value;
|
|
45
67
|
}
|
|
46
68
|
}
|
|
47
69
|
|
|
70
|
+
if (Nife.isEmpty(finalQuery))
|
|
71
|
+
return;
|
|
72
|
+
|
|
73
|
+
Object.defineProperties(finalQuery, {
|
|
74
|
+
'_mythixQuery': {
|
|
75
|
+
writable: false,
|
|
76
|
+
enumberable: false,
|
|
77
|
+
configurable: false,
|
|
78
|
+
value: true,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
48
82
|
return finalQuery;
|
|
49
83
|
}
|
|
50
84
|
|
|
51
|
-
static
|
|
52
|
-
Klass.
|
|
53
|
-
Klass.
|
|
85
|
+
static onModelClassFinalized(Klass) {
|
|
86
|
+
Klass.getDefaultOrderBy = Klass.getDefaultOrderBy.bind(this, Klass);
|
|
87
|
+
Klass.prepareQueryOptions = Klass.prepareQueryOptions.bind(this, Klass);
|
|
88
|
+
Klass.bulkUpdate = Klass.bulkUpdate.bind(this, Klass);
|
|
89
|
+
Klass.all = Klass.all.bind(this, Klass);
|
|
90
|
+
Klass.where = Klass.where.bind(this, Klass);
|
|
91
|
+
Klass.rowCount = Klass.rowCount.bind(this, Klass);
|
|
92
|
+
Klass.first = Klass.first.bind(this, Klass);
|
|
93
|
+
Klass.last = Klass.last.bind(this, Klass);
|
|
54
94
|
|
|
55
95
|
return Klass;
|
|
56
96
|
}
|
|
57
97
|
|
|
58
|
-
static
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
static getDefaultOrderBy(Model) {
|
|
99
|
+
return [ Model.getPrimaryKeyFieldName() ];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static prepareQueryOptions(Model, conditions, _order) {
|
|
103
|
+
const Ops = Sequelize.Op;
|
|
104
|
+
var options;
|
|
105
|
+
var query;
|
|
106
|
+
|
|
107
|
+
if (conditions && Nife.isNotEmpty(conditions.where)) {
|
|
108
|
+
query = conditions.where;
|
|
109
|
+
options = Object.assign({}, conditions);
|
|
110
|
+
} else if (conditions) {
|
|
111
|
+
query = Model.prepareWhereStatement(conditions);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var order = _order;
|
|
115
|
+
if (!options && Nife.instanceOf(order, 'object')) {
|
|
116
|
+
options = Object.assign({}, order);
|
|
117
|
+
order = undefined;
|
|
118
|
+
} else if (!options) {
|
|
119
|
+
options = {};
|
|
120
|
+
}
|
|
61
121
|
|
|
62
122
|
if (Nife.isNotEmpty(query))
|
|
63
123
|
options.where = query;
|
|
64
124
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
125
|
+
if (!order && options.defaultOrder !== false) {
|
|
126
|
+
if (options.order) {
|
|
127
|
+
order = options.order;
|
|
128
|
+
} else {
|
|
129
|
+
if (typeof Model.getDefaultOrderBy === 'function')
|
|
130
|
+
order = Model.getDefaultOrderBy();
|
|
131
|
+
else
|
|
132
|
+
order = [ Model.getPrimaryKeyFieldName() ];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
68
135
|
|
|
69
136
|
options.order = order;
|
|
137
|
+
if (!options.hasOwnProperty('distinct'))
|
|
138
|
+
options.distinct = true;
|
|
139
|
+
|
|
140
|
+
// If no "where" clause was specified, then grab everything
|
|
141
|
+
if (!options.where)
|
|
142
|
+
options.where = { [Model.getPrimaryKeyFieldName()]: { [Ops.not]: null } };
|
|
143
|
+
|
|
144
|
+
if (options.debug)
|
|
145
|
+
console.log('QUERY OPTIONS: ', options);
|
|
146
|
+
|
|
147
|
+
return options;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static where(Model, conditions) {
|
|
151
|
+
return Model.prepareWhereStatement(conditions);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static async rowCount(Model, conditions, options) {
|
|
155
|
+
return await Model.count(Model.prepareQueryOptions(conditions, options));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static async bulkUpdate(Model, attrs, conditions) {
|
|
159
|
+
return await Model.update(attrs, Model.prepareQueryOptions(conditions, { distinct: false }));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static async all(Model, conditions, order) {
|
|
163
|
+
return await Model.findAll(Model.prepareQueryOptions(conditions, order));
|
|
164
|
+
}
|
|
70
165
|
|
|
71
|
-
|
|
166
|
+
static async first(Model, conditions, order) {
|
|
167
|
+
return await Model.findOne(Model.prepareQueryOptions(conditions, order));
|
|
72
168
|
}
|
|
73
169
|
|
|
74
170
|
static async last(Model, conditions, _order) {
|
|
75
171
|
var order = _order;
|
|
76
172
|
if (!order)
|
|
77
|
-
order = [
|
|
173
|
+
order = [ 'createdAt', 'DESC' ];
|
|
78
174
|
|
|
79
175
|
return await Model.first(conditions, order);
|
|
80
176
|
}
|
package/src/tasks/task-base.js
CHANGED
package/src/tasks/task-utils.js
CHANGED
package/src/utils/http-utils.js
CHANGED
|
@@ -59,9 +59,12 @@ function makeRequest(requestOptions) {
|
|
|
59
59
|
var url = new URL(requestOptions.url);
|
|
60
60
|
var data = (!method.match(/^(GET|HEAD)$/i) && requestOptions.data) ? requestOptions.data : undefined;
|
|
61
61
|
var extraConfig = {};
|
|
62
|
+
var headers = Object.assign({
|
|
63
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
|
|
64
|
+
}, defaultHeaders || {}, requestOptions.headers || {});
|
|
62
65
|
|
|
63
66
|
if (data) {
|
|
64
|
-
if (Nife.get(
|
|
67
|
+
if (Nife.get(headers, 'Content-Type', '').match(/application\/json/))
|
|
65
68
|
data = JSON.stringify(data);
|
|
66
69
|
|
|
67
70
|
extraConfig = {
|
|
@@ -77,9 +80,7 @@ function makeRequest(requestOptions) {
|
|
|
77
80
|
port: url.port,
|
|
78
81
|
path: `${url.pathname}${url.search}`,
|
|
79
82
|
method,
|
|
80
|
-
headers
|
|
81
|
-
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
|
|
82
|
-
}, defaultHeaders || {}),
|
|
83
|
+
headers,
|
|
83
84
|
}, requestOptions, extraConfig);
|
|
84
85
|
|
|
85
86
|
delete options.data;
|
package/src/utils/index.js
CHANGED
|
@@ -7,11 +7,13 @@ const {
|
|
|
7
7
|
|
|
8
8
|
const HTTPUtils = require('./http-utils');
|
|
9
9
|
const CryptoUtils = require('./crypto-utils');
|
|
10
|
+
const TestUtils = require('./test-utils');
|
|
10
11
|
|
|
11
12
|
module.exports = {
|
|
12
13
|
CryptoUtils,
|
|
13
14
|
fileNameWithoutExtension,
|
|
14
15
|
HTTPUtils,
|
|
16
|
+
TestUtils,
|
|
15
17
|
walkDir,
|
|
16
18
|
wrapConfig,
|
|
17
19
|
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const Nife = require('nife');
|
|
2
|
+
const { Logger } = require('../logger');
|
|
3
|
+
const HTTPUtils = require('./http-utils');
|
|
4
|
+
|
|
5
|
+
function createTestApplication(Application) {
|
|
6
|
+
const Klass = class TestApplication extends Application {
|
|
7
|
+
static APP_NAME = `${(Nife.isNotEmpty(Application.APP_NAME)) ? Application.APP_NAME : 'mythix'}_test`;
|
|
8
|
+
|
|
9
|
+
constructor(_opts) {
|
|
10
|
+
var opts = Nife.extend(true, {
|
|
11
|
+
autoReload: false,
|
|
12
|
+
runTasks: false,
|
|
13
|
+
testMode: true,
|
|
14
|
+
}, _opts || {});
|
|
15
|
+
|
|
16
|
+
super(opts);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getDBTablePrefix(userSpecifiedPrefix) {
|
|
20
|
+
var prefix = super.getDBTablePrefix(userSpecifiedPrefix);
|
|
21
|
+
return `${prefix.replace(/_test/g, '')}_test_`.replace(/_+/g, '_');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getTestingDatabaseConfig() {
|
|
25
|
+
return { dialect: 'sqlite', logging: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getTestingHTTPServerConfig(options) {
|
|
29
|
+
return Object.assign({}, options || {}, {
|
|
30
|
+
host: 'localhost',
|
|
31
|
+
port: 0, // Select a random port
|
|
32
|
+
https: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getTestingLoggerConfig(loggerOpts) {
|
|
37
|
+
return Object.assign({}, loggerOpts, {
|
|
38
|
+
level: Logger.ERROR,
|
|
39
|
+
writer: null,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
createLogger(loggerOpts, Logger) {
|
|
44
|
+
return super.createLogger(this.getTestingLoggerConfig(loggerOpts), Logger);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async connectToDatabase() {
|
|
48
|
+
var connection = await super.connectToDatabase(this.getTestingDatabaseConfig());
|
|
49
|
+
return connection;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async createHTTPServer(options) {
|
|
53
|
+
var httpServerConfig = this.getTestingHTTPServerConfig(options);
|
|
54
|
+
var server = await super.createHTTPServer(httpServerConfig);
|
|
55
|
+
|
|
56
|
+
Object.defineProperties(this, {
|
|
57
|
+
'host': {
|
|
58
|
+
writable: true,
|
|
59
|
+
enumberable: false,
|
|
60
|
+
configurable: true,
|
|
61
|
+
value: {
|
|
62
|
+
hostname: httpServerConfig.host,
|
|
63
|
+
port: server.server.address().port,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.setDefaultURL(`http://${this.host.hostname}:${this.host.port}/`);
|
|
69
|
+
|
|
70
|
+
return server;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async start(...args) {
|
|
74
|
+
var result = await super.start(...args);
|
|
75
|
+
|
|
76
|
+
var dbConnection = this.getDBConnection();
|
|
77
|
+
await dbConnection.sync({ force: true, logging: false });
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async truncateAllTables(exclude) {
|
|
83
|
+
var dbConnection = this.getDBConnection();
|
|
84
|
+
var models = this.getModels();
|
|
85
|
+
var modelNames = Object.keys(models);
|
|
86
|
+
|
|
87
|
+
await dbConnection.query('PRAGMA foreign_keys = OFF');
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
for (var i = 0, il = modelNames.length; i < il; i++) {
|
|
91
|
+
var modelName = modelNames[i];
|
|
92
|
+
if (exclude && exclude.indexOf(modelName) >= 0)
|
|
93
|
+
continue;
|
|
94
|
+
|
|
95
|
+
var model = models[modelName];
|
|
96
|
+
await dbConnection.query(`DELETE FROM ${model.tableName}`);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
await dbConnection.query('PRAGMA foreign_keys = ON');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getDefaultURL(...args) {
|
|
104
|
+
return HTTPUtils.getDefaultURL(...args);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setDefaultURL(...args) {
|
|
108
|
+
return HTTPUtils.setDefaultURL(...args);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getDefaultHeader(...args) {
|
|
112
|
+
return HTTPUtils.getDefaultHeader(...args);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getDefaultHeaders(...args) {
|
|
116
|
+
return HTTPUtils.getDefaultHeaders(...args);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async setDefaultHeader(...args) {
|
|
120
|
+
return HTTPUtils.setDefaultHeader(...args);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async setDefaultHeaders(...args) {
|
|
124
|
+
return HTTPUtils.setDefaultHeaders(...args);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async request(...args) {
|
|
128
|
+
return HTTPUtils.request(...args);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async get(...args) {
|
|
132
|
+
return HTTPUtils.get(...args);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async post(...args) {
|
|
136
|
+
return HTTPUtils.post(...args);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async put(...args) {
|
|
140
|
+
return HTTPUtils.put(...args);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async delete(...args) {
|
|
144
|
+
return HTTPUtils.delete(...args);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async head(...args) {
|
|
148
|
+
return HTTPUtils.head(...args);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async options(...args) {
|
|
152
|
+
return HTTPUtils.options(...args);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return Klass;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
createTestApplication,
|
|
161
|
+
};
|