mythix 1.0.1
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/.vscode/settings.json +7 -0
- package/LICENSE +21 -0
- package/README.md +716 -0
- package/package.json +34 -0
- package/spec/controller-utils-spec.js +141 -0
- package/spec/support/jasmine.json +13 -0
- package/spec/utils-spec.js +10 -0
- package/src/application.js +934 -0
- package/src/cli/cli-utils.js +388 -0
- package/src/cli/index.js +3 -0
- package/src/cli/migrations/makemigrations-command.js +94 -0
- package/src/cli/migrations/migrate-command.js +105 -0
- package/src/cli/migrations/migration-utils.js +952 -0
- package/src/cli/serve-command.js +32 -0
- package/src/cli/shell-command.js +82 -0
- package/src/controllers/controller-base.js +83 -0
- package/src/controllers/controller-utils.js +415 -0
- package/src/controllers/index.js +20 -0
- package/src/http-server/http-errors.js +68 -0
- package/src/http-server/http-server.js +359 -0
- package/src/http-server/http-utils.js +23 -0
- package/src/http-server/index.js +17 -0
- package/src/http-server/middleware/default-middleware.js +106 -0
- package/src/http-server/middleware/index.js +5 -0
- package/src/index.js +32 -0
- package/src/logger.js +211 -0
- package/src/models/index.js +14 -0
- package/src/models/model-utils.js +259 -0
- package/src/models/model.js +85 -0
- package/src/tasks/index.js +7 -0
- package/src/tasks/task-base.js +120 -0
- package/src/tasks/task-utils.js +127 -0
- package/src/utils/config-utils.js +35 -0
- package/src/utils/crypto-utils.js +36 -0
- package/src/utils/file-utils.js +43 -0
- package/src/utils/http-utils.js +191 -0
- package/src/utils/index.js +17 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
const Nife = require('nife');
|
|
2
|
+
const Path = require('path');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
const { Sequelize } = require('sequelize');
|
|
5
|
+
const chokidar = require('chokidar');
|
|
6
|
+
const { Logger } = require('./logger');
|
|
7
|
+
const { HTTPServer } = require('./http-server');
|
|
8
|
+
const { buildModelRelations } = require('./models/model-utils');
|
|
9
|
+
const { buildRoutes } = require('./controllers/controller-utils');
|
|
10
|
+
const {
|
|
11
|
+
wrapConfig,
|
|
12
|
+
fileNameWithoutExtension,
|
|
13
|
+
walkDir,
|
|
14
|
+
} = require('./utils');
|
|
15
|
+
|
|
16
|
+
function nowInSeconds() {
|
|
17
|
+
return Math.floor(Date.now() / 1000);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Trace what is requesting the application exit
|
|
21
|
+
|
|
22
|
+
// (function doExit(_exit) {
|
|
23
|
+
// process.exit = function() {
|
|
24
|
+
// console.trace('EXIT');
|
|
25
|
+
// return _exit.apply(process, arguments);
|
|
26
|
+
// };
|
|
27
|
+
// })(process.exit);
|
|
28
|
+
|
|
29
|
+
var globalTaskRunID = 1;
|
|
30
|
+
|
|
31
|
+
class Application extends EventEmitter {
|
|
32
|
+
static APP_NAME = 'mythix';
|
|
33
|
+
|
|
34
|
+
constructor(_opts) {
|
|
35
|
+
super();
|
|
36
|
+
|
|
37
|
+
var ROOT_PATH = (_opts && _opts.rootPath) ? _opts.rootPath : Path.resolve(__dirname);
|
|
38
|
+
|
|
39
|
+
var opts = Nife.extend(true, {
|
|
40
|
+
appName: this.constructor.APP_NAME,
|
|
41
|
+
rootPath: ROOT_PATH,
|
|
42
|
+
configPath: Path.resolve(ROOT_PATH, 'config'),
|
|
43
|
+
migrationsPath: Path.resolve(ROOT_PATH, 'migrations'),
|
|
44
|
+
modelsPath: Path.resolve(ROOT_PATH, 'models'),
|
|
45
|
+
seedersPath: Path.resolve(ROOT_PATH, 'seeders'),
|
|
46
|
+
controllersPath: Path.resolve(ROOT_PATH, 'controllers'),
|
|
47
|
+
templatesPath: Path.resolve(ROOT_PATH, 'templates'),
|
|
48
|
+
commandsPath: Path.resolve(ROOT_PATH, 'commands'),
|
|
49
|
+
tasksPath: Path.resolve(ROOT_PATH, 'tasks'),
|
|
50
|
+
logger: {
|
|
51
|
+
rootPath: ROOT_PATH,
|
|
52
|
+
},
|
|
53
|
+
database: {},
|
|
54
|
+
httpServer: {
|
|
55
|
+
routeParserTypes: undefined,
|
|
56
|
+
middleware: null,
|
|
57
|
+
},
|
|
58
|
+
autoReload: (process.env.NODE_ENV || 'development') === 'development',
|
|
59
|
+
exitOnShutdown: null,
|
|
60
|
+
runTasks: true,
|
|
61
|
+
}, _opts || {});
|
|
62
|
+
|
|
63
|
+
Object.defineProperties(this, {
|
|
64
|
+
'dbConnection': {
|
|
65
|
+
writable: true,
|
|
66
|
+
enumerable: false,
|
|
67
|
+
configurable: true,
|
|
68
|
+
value: null,
|
|
69
|
+
},
|
|
70
|
+
'isStarted': {
|
|
71
|
+
writable: true,
|
|
72
|
+
enumerable: false,
|
|
73
|
+
configurable: true,
|
|
74
|
+
value: false,
|
|
75
|
+
},
|
|
76
|
+
'isStopping': {
|
|
77
|
+
writable: true,
|
|
78
|
+
enumerable: false,
|
|
79
|
+
configurable: true,
|
|
80
|
+
value: false,
|
|
81
|
+
},
|
|
82
|
+
'options': {
|
|
83
|
+
writable: false,
|
|
84
|
+
enumerable: false,
|
|
85
|
+
configurable: true,
|
|
86
|
+
value: opts,
|
|
87
|
+
},
|
|
88
|
+
'controllers': {
|
|
89
|
+
writable: true,
|
|
90
|
+
enumerable: false,
|
|
91
|
+
configurable: true,
|
|
92
|
+
value: {},
|
|
93
|
+
},
|
|
94
|
+
'models': {
|
|
95
|
+
writable: true,
|
|
96
|
+
enumerable: false,
|
|
97
|
+
configurable: true,
|
|
98
|
+
value: {},
|
|
99
|
+
},
|
|
100
|
+
'server': {
|
|
101
|
+
writable: true,
|
|
102
|
+
enumerable: false,
|
|
103
|
+
configurable: true,
|
|
104
|
+
value: null,
|
|
105
|
+
},
|
|
106
|
+
'fileWatcher': {
|
|
107
|
+
writable: true,
|
|
108
|
+
enumerable: false,
|
|
109
|
+
configurable: true,
|
|
110
|
+
value: null,
|
|
111
|
+
},
|
|
112
|
+
'tasks': {
|
|
113
|
+
writable: true,
|
|
114
|
+
enumerable: false,
|
|
115
|
+
configurable: true,
|
|
116
|
+
value: {},
|
|
117
|
+
},
|
|
118
|
+
'taskInfo': {
|
|
119
|
+
writable: true,
|
|
120
|
+
enumerable: false,
|
|
121
|
+
configurable: true,
|
|
122
|
+
value: {},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
Object.defineProperties(this, {
|
|
127
|
+
'config': {
|
|
128
|
+
writable: false,
|
|
129
|
+
enumerable: false,
|
|
130
|
+
configurable: true,
|
|
131
|
+
value: this.loadConfig(opts.configPath),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
Object.defineProperties(this, {
|
|
136
|
+
'logger': {
|
|
137
|
+
writable: true,
|
|
138
|
+
enumerable: false,
|
|
139
|
+
configurable: true,
|
|
140
|
+
value: this.createLogger(
|
|
141
|
+
Object.assign({}, opts.logger || {}, this.getConfigValue('logger', {})),
|
|
142
|
+
Logger,
|
|
143
|
+
),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
this.bindToProcessSignals();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async autoReload(_set, shuttingDown) {
|
|
151
|
+
var options = this.getOptions();
|
|
152
|
+
if (arguments.length === 0)
|
|
153
|
+
return options.autoReload;
|
|
154
|
+
|
|
155
|
+
var set = !!_set;
|
|
156
|
+
|
|
157
|
+
if (!shuttingDown)
|
|
158
|
+
options.autoReload = set;
|
|
159
|
+
|
|
160
|
+
if (this.fileWatcher) {
|
|
161
|
+
try {
|
|
162
|
+
await this.fileWatcher.close();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (shuttingDown)
|
|
169
|
+
return;
|
|
170
|
+
|
|
171
|
+
if (!set)
|
|
172
|
+
return;
|
|
173
|
+
|
|
174
|
+
var getFileScope = (path) => {
|
|
175
|
+
if (path.substring(0, options.controllersPath.length) === options.controllersPath)
|
|
176
|
+
return 'controllers';
|
|
177
|
+
|
|
178
|
+
if (path.substring(0, options.modelsPath.length) === options.modelsPath)
|
|
179
|
+
return 'models';
|
|
180
|
+
|
|
181
|
+
if (path.substring(0, options.tasksPath.length) === options.tasksPath)
|
|
182
|
+
return 'tasks';
|
|
183
|
+
|
|
184
|
+
return 'default';
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const filesChanged = (eventName, path) => {
|
|
188
|
+
if (filesChangedTimeout)
|
|
189
|
+
clearTimeout(filesChangedTimeout);
|
|
190
|
+
|
|
191
|
+
var scopeName = getFileScope(path);
|
|
192
|
+
var scope = filesChangedQueue[scopeName];
|
|
193
|
+
if (!scope)
|
|
194
|
+
scope = filesChangedQueue[scopeName] = {};
|
|
195
|
+
|
|
196
|
+
scope[path] = eventName;
|
|
197
|
+
|
|
198
|
+
filesChangedTimeout = setTimeout(() => {
|
|
199
|
+
this.watchedFilesChanged(Object.assign({}, filesChangedQueue));
|
|
200
|
+
|
|
201
|
+
filesChangedTimeout = null;
|
|
202
|
+
filesChangedQueue = {};
|
|
203
|
+
}, 500);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
var filesChangedQueue = {};
|
|
207
|
+
var filesChangedTimeout;
|
|
208
|
+
|
|
209
|
+
this.fileWatcher = chokidar.watch([ options.modelsPath, options.controllersPath, options.tasksPath ], {
|
|
210
|
+
persistent: true,
|
|
211
|
+
followSymlinks: true,
|
|
212
|
+
usePolling: false,
|
|
213
|
+
ignoreInitial: true,
|
|
214
|
+
interval: 200,
|
|
215
|
+
binaryInterval: 500,
|
|
216
|
+
depth: 10,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this.fileWatcher.on('all', filesChanged);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async watchedFilesChanged(files) {
|
|
223
|
+
const flushRequireCache = (path) => {
|
|
224
|
+
try {
|
|
225
|
+
delete require.cache[require.resolve(path)];
|
|
226
|
+
} catch (error) {}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const flushRequireCacheForFiles = (type, files) => {
|
|
230
|
+
for (var i = 0, il = files.length; i < il; i++) {
|
|
231
|
+
var fileName = files[i];
|
|
232
|
+
flushRequireCache(fileName);
|
|
233
|
+
|
|
234
|
+
this.getLogger().info(`Loading ${type} ${fileName}...`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
var options = this.getOptions();
|
|
239
|
+
var handlers = {
|
|
240
|
+
'controllers': {
|
|
241
|
+
type: 'controller',
|
|
242
|
+
reloadHandler: async () => {
|
|
243
|
+
if (!this.server)
|
|
244
|
+
return;
|
|
245
|
+
|
|
246
|
+
var controllers = await this.loadControllers(options.controllersPath, this.server);
|
|
247
|
+
this.controllers = controllers;
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
'models': {
|
|
251
|
+
type: 'model',
|
|
252
|
+
reloadHandler: async () => {
|
|
253
|
+
if (!options.database)
|
|
254
|
+
return;
|
|
255
|
+
|
|
256
|
+
var models = await this.loadModels(options.modelsPath, options.database);
|
|
257
|
+
this.models = models;
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
'tasks': {
|
|
261
|
+
type: 'tasks',
|
|
262
|
+
reloadHandler: async () => {
|
|
263
|
+
await this.waitForAllTasksToFinish();
|
|
264
|
+
|
|
265
|
+
var tasks = await this.loadTasks(options.tasksPath, options.database);
|
|
266
|
+
|
|
267
|
+
this.tasks = tasks;
|
|
268
|
+
this.taskInfo = { _startTime: nowInSeconds() };
|
|
269
|
+
|
|
270
|
+
this.startTasks();
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
var handlerNames = Object.keys(handlers);
|
|
276
|
+
for (var i = 0, il = handlerNames.length; i < il; i++) {
|
|
277
|
+
var handlerName = handlerNames[i];
|
|
278
|
+
var handler = handlers[handlerName];
|
|
279
|
+
var scope = files[handlerName];
|
|
280
|
+
var fileNames = Object.keys(scope || {});
|
|
281
|
+
|
|
282
|
+
if (Nife.isEmpty(fileNames))
|
|
283
|
+
continue;
|
|
284
|
+
|
|
285
|
+
var {
|
|
286
|
+
type,
|
|
287
|
+
reloadHandler,
|
|
288
|
+
} = handler;
|
|
289
|
+
|
|
290
|
+
flushRequireCacheForFiles(type, fileNames);
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
await reloadHandler();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.getLogger().error(`Error while attempting to reload ${handlerName}`, error);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
bindToProcessSignals() {
|
|
301
|
+
process.on('SIGINT', this.stop.bind(this));
|
|
302
|
+
process.on('SIGTERM', this.stop.bind(this));
|
|
303
|
+
process.on('SIGHUP', this.stop.bind(this));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
getOptions() {
|
|
307
|
+
return this.options;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
setOptions(opts) {
|
|
311
|
+
if (!opts)
|
|
312
|
+
return;
|
|
313
|
+
|
|
314
|
+
var options = this.getOptions();
|
|
315
|
+
Nife.extend(true, options, opts);
|
|
316
|
+
|
|
317
|
+
return this;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
loadConfig(configPath) {
|
|
321
|
+
try {
|
|
322
|
+
const config = require(configPath);
|
|
323
|
+
return wrapConfig(config);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
this.getLogger().error(`Error while trying to load application configuration ${configPath}: `, error);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getConfigValue(key, defaultValue, type) {
|
|
331
|
+
var result = this.config.ENV(key, defaultValue);
|
|
332
|
+
|
|
333
|
+
// Coerce to type, if type was specified
|
|
334
|
+
if (type)
|
|
335
|
+
Nife.coerceValue(result, type);
|
|
336
|
+
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
getConfig() {
|
|
341
|
+
return this.config;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
setConfig(opts) {
|
|
345
|
+
Nife.extend(true, this.config.CONFIG, opts);
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
getApplicationName() {
|
|
350
|
+
var options = this.getOptions();
|
|
351
|
+
return options.appName;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
getModelFilePaths(modelsPath) {
|
|
355
|
+
return walkDir(modelsPath, {
|
|
356
|
+
filter: (fullFileName, fileName, stats) => {
|
|
357
|
+
if (fileName.match(/^_/))
|
|
358
|
+
return false;
|
|
359
|
+
|
|
360
|
+
if (stats.isFile() && !fileNameWithoutExtension(fileName).match(/-model$/))
|
|
361
|
+
return false;
|
|
362
|
+
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
loadModels(modelsPath, dbConfig) {
|
|
369
|
+
var modelFiles = this.getModelFilePaths(modelsPath);
|
|
370
|
+
var models = {};
|
|
371
|
+
var args = { application: this, Sequelize, connection: this.dbConnection, dbConfig };
|
|
372
|
+
|
|
373
|
+
for (var i = 0, il = modelFiles.length; i < il; i++) {
|
|
374
|
+
var modelFile = modelFiles[i];
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
var modelGenerator = require(modelFile);
|
|
378
|
+
if (modelGenerator['default'] && typeof modelGenerator['default'] === 'function')
|
|
379
|
+
modelGenerator = modelGenerator['default'];
|
|
380
|
+
|
|
381
|
+
Object.assign(models, modelGenerator(args));
|
|
382
|
+
} catch (error) {
|
|
383
|
+
this.getLogger().error(`Error while loading model ${modelFile}: `, error);
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
buildModelRelations(models);
|
|
389
|
+
|
|
390
|
+
Object.defineProperties(models, {
|
|
391
|
+
'_files': {
|
|
392
|
+
writable: true,
|
|
393
|
+
enumberable: false,
|
|
394
|
+
configurable: true,
|
|
395
|
+
value: modelFiles,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return models;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
getModel(name) {
|
|
403
|
+
var models = this.models;
|
|
404
|
+
return models[name];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
getModels() {
|
|
408
|
+
return this.models || {};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
getControllerFilePaths(controllersPath) {
|
|
412
|
+
return walkDir(controllersPath, {
|
|
413
|
+
filter: (fullFileName, fileName, stats) => {
|
|
414
|
+
if (fileName.match(/^_/))
|
|
415
|
+
return false;
|
|
416
|
+
|
|
417
|
+
if (stats.isFile() && !fileNameWithoutExtension(fileName).match(/-controller$/))
|
|
418
|
+
return false;
|
|
419
|
+
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
loadControllers(controllersPath, server) {
|
|
426
|
+
var controllerFiles = this.getControllerFilePaths(controllersPath);
|
|
427
|
+
var controllers = {};
|
|
428
|
+
var args = { application: this, server };
|
|
429
|
+
|
|
430
|
+
for (var i = 0, il = controllerFiles.length; i < il; i++) {
|
|
431
|
+
var controllerFile = controllerFiles[i];
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
var controllerGenerator = require(controllerFile);
|
|
435
|
+
if (controllerGenerator['default'] && typeof controllerGenerator['default'] === 'function')
|
|
436
|
+
controllerGenerator = controllerGenerator['default'];
|
|
437
|
+
|
|
438
|
+
Object.assign(controllers, controllerGenerator(args));
|
|
439
|
+
} catch (error) {
|
|
440
|
+
this.getLogger().error(`Error while loading model ${controllerFile}: `, error);
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
Object.defineProperties(controllers, {
|
|
446
|
+
'_files': {
|
|
447
|
+
writable: true,
|
|
448
|
+
enumberable: false,
|
|
449
|
+
configurable: true,
|
|
450
|
+
value: controllerFiles,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return controllers;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
getController(name) {
|
|
458
|
+
var controllers = this.controllers;
|
|
459
|
+
var controllerName = name.replace(/(.*?)\b\w+$/, '$1');
|
|
460
|
+
var methodName = name.substring(controllerName.length);
|
|
461
|
+
if (!methodName)
|
|
462
|
+
methodName = undefined;
|
|
463
|
+
|
|
464
|
+
controllerName = controllerName.replace(/\W+$/g, '');
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
controller: Nife.get(controllers, controllerName),
|
|
468
|
+
controllerMethod: methodName,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
getRoutes() {
|
|
473
|
+
throw new Error('Error: child application expected to implement "getRoutes" method');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
getCustomRouteParserTypes() {
|
|
477
|
+
var options = this.getOptions();
|
|
478
|
+
return options.routeParserTypes;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
buildRoutes(server, routes) {
|
|
482
|
+
var customParserTypes = this.getCustomRouteParserTypes(server, routes);
|
|
483
|
+
return buildRoutes(routes, customParserTypes);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
getTaskFilePaths(tasksPath) {
|
|
487
|
+
return walkDir(tasksPath, {
|
|
488
|
+
filter: (fullFileName, fileName, stats) => {
|
|
489
|
+
if (fileName.match(/^_/))
|
|
490
|
+
return false;
|
|
491
|
+
|
|
492
|
+
if (stats.isFile() && !fileNameWithoutExtension(fileName).match(/-task$/))
|
|
493
|
+
return false;
|
|
494
|
+
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
loadTasks(tasksPath, dbConfig) {
|
|
501
|
+
var taskFiles = this.getTaskFilePaths(tasksPath);
|
|
502
|
+
var tasks = {};
|
|
503
|
+
var args = { application: this, Sequelize, connection: this.dbConnection, dbConfig };
|
|
504
|
+
|
|
505
|
+
for (var i = 0, il = taskFiles.length; i < il; i++) {
|
|
506
|
+
var taskFile = taskFiles[i];
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
var taskGenerator = require(taskFile);
|
|
510
|
+
if (taskGenerator['default'] && typeof taskGenerator['default'] === 'function')
|
|
511
|
+
taskGenerator = taskGenerator['default'];
|
|
512
|
+
|
|
513
|
+
Object.assign(tasks, taskGenerator(args));
|
|
514
|
+
} catch (error) {
|
|
515
|
+
this.getLogger().error(`Error while loading task ${taskFile}: `, error);
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
Object.defineProperties(tasks, {
|
|
521
|
+
'_files': {
|
|
522
|
+
writable: true,
|
|
523
|
+
enumberable: false,
|
|
524
|
+
configurable: true,
|
|
525
|
+
value: taskFiles,
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
return tasks;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async runTasks() {
|
|
533
|
+
const executeTask = (TaskKlass, taskIndex, taskInfo, lastTime, currentTime, diff) => {
|
|
534
|
+
const createTaskLogger = () => {
|
|
535
|
+
var logger = this.getLogger();
|
|
536
|
+
return logger.clone({ formatter: (output) => `[[ Running task ${taskName}[${taskIndex}](${runID}) @ ${currentTime} ]]: ${output}`});
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const successResult = (value) => {
|
|
540
|
+
taskInfo.failedCount = 0;
|
|
541
|
+
promise.resolve(value);
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const errorResult = (error) => {
|
|
545
|
+
var thisLogger = logger;
|
|
546
|
+
if (!thisLogger)
|
|
547
|
+
thisLogger = this.getLogger();
|
|
548
|
+
|
|
549
|
+
thisLogger.error(`Task "${taskName}[${taskIndex}]" failed with an error: `, error);
|
|
550
|
+
|
|
551
|
+
promise.reject(error);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const runTask = () => {
|
|
555
|
+
var result = taskInstance.execute(lastTime, currentTime, diff);
|
|
556
|
+
|
|
557
|
+
if (Nife.instanceOf(result, 'promise')) {
|
|
558
|
+
result.then(
|
|
559
|
+
successResult,
|
|
560
|
+
errorResult,
|
|
561
|
+
);
|
|
562
|
+
} else {
|
|
563
|
+
promise.resolve(result);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
var taskName = TaskKlass.taskName;
|
|
568
|
+
var promise = Nife.createResolvable();
|
|
569
|
+
var taskInstance = taskInfo.taskInstance;
|
|
570
|
+
var runID;
|
|
571
|
+
var logger;
|
|
572
|
+
|
|
573
|
+
if (!TaskKlass.keepAlive || !taskInstance) {
|
|
574
|
+
globalTaskRunID++;
|
|
575
|
+
runID = `${Math.floor(Date.now() + (Math.random() * 1000000)) + globalTaskRunID}-${taskIndex}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
taskInfo.runID = runID;
|
|
579
|
+
|
|
580
|
+
// No op, since promises are handled differently here
|
|
581
|
+
promise.then(() => {}, () => {});
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
if (!TaskKlass.keepAlive || !taskInstance) {
|
|
585
|
+
logger = createTaskLogger();
|
|
586
|
+
taskInstance = new TaskKlass(this, logger, runID, { lastTime, currentTime, diff });
|
|
587
|
+
|
|
588
|
+
taskInfo.taskInstance = taskInstance;
|
|
589
|
+
|
|
590
|
+
taskInstance.start().then(runTask, errorResult);
|
|
591
|
+
} else {
|
|
592
|
+
logger = taskInstance.getLogger();
|
|
593
|
+
runTask();
|
|
594
|
+
}
|
|
595
|
+
} catch (error) {
|
|
596
|
+
errorResult(error);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return promise;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const handleTask = (taskName, taskKlass, taskInfo, taskIndex, failAfterAttempts) => {
|
|
603
|
+
var lastTime = taskInfo.lastTime;
|
|
604
|
+
var startTime = lastTime || allTasksInfo._startTime;
|
|
605
|
+
var diff = (currentTime - startTime);
|
|
606
|
+
var lastRunStatus = (taskInfo && taskInfo.promise && taskInfo.promise.status());
|
|
607
|
+
|
|
608
|
+
if (lastRunStatus === 'pending')
|
|
609
|
+
return;
|
|
610
|
+
|
|
611
|
+
if (lastRunStatus === 'rejected') {
|
|
612
|
+
taskInfo.promise = null;
|
|
613
|
+
taskInfo.failedCount++;
|
|
614
|
+
|
|
615
|
+
if (taskInfo.failedCount >= failAfterAttempts)
|
|
616
|
+
this.getLogger().error(`Task "${taskName}[${taskIndex}]" failed permanently after ${taskInfo.failedCount} failed attempts`);
|
|
617
|
+
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (taskInfo.failedCount >= failAfterAttempts)
|
|
622
|
+
return;
|
|
623
|
+
|
|
624
|
+
if (!taskKlass.shouldRun(taskIndex, lastTime, currentTime, diff))
|
|
625
|
+
return;
|
|
626
|
+
|
|
627
|
+
taskInfo.lastTime = currentTime;
|
|
628
|
+
taskInfo.promise = executeTask(taskKlass, taskIndex, taskInfo, lastTime, currentTime, diff);
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const handleTaskQueue = (taskName, taskKlass, infoForTasks) => {
|
|
632
|
+
var failAfterAttempts = taskKlass.failAfterAttempts || 5;
|
|
633
|
+
var workers = taskKlass.workers || 1;
|
|
634
|
+
|
|
635
|
+
for (var taskIndex = 0; taskIndex < workers; taskIndex++) {
|
|
636
|
+
var taskInfo = infoForTasks[taskIndex];
|
|
637
|
+
if (!taskInfo)
|
|
638
|
+
taskInfo = infoForTasks[taskIndex] = { failedCount: 0, promise: null, stop: false };
|
|
639
|
+
|
|
640
|
+
if (taskInfo.stop)
|
|
641
|
+
continue;
|
|
642
|
+
|
|
643
|
+
handleTask(taskName, taskKlass, taskInfo, taskIndex, failAfterAttempts);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
var currentTime = nowInSeconds();
|
|
648
|
+
var allTasksInfo = this.taskInfo;
|
|
649
|
+
var tasks = this.tasks;
|
|
650
|
+
var taskNames = Object.keys(tasks);
|
|
651
|
+
|
|
652
|
+
for (var i = 0, il = taskNames.length; i < il; i++) {
|
|
653
|
+
var taskName = taskNames[i];
|
|
654
|
+
var taskKlass = tasks[taskName];
|
|
655
|
+
if (taskKlass.enabled === false)
|
|
656
|
+
continue;
|
|
657
|
+
|
|
658
|
+
var tasksInfo = allTasksInfo[taskName];
|
|
659
|
+
if (!tasksInfo)
|
|
660
|
+
tasksInfo = allTasksInfo[taskName] = [];
|
|
661
|
+
|
|
662
|
+
handleTaskQueue(taskName, taskKlass, tasksInfo);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
stopTasks() {
|
|
667
|
+
var intervalTimerID = (this.tasks && this.tasks._intervalTimerID);
|
|
668
|
+
if (intervalTimerID) {
|
|
669
|
+
clearInterval(intervalTimerID);
|
|
670
|
+
this.tasks._intervalTimerID = null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async startTasks(flushTaskInfo) {
|
|
675
|
+
this.stopTasks();
|
|
676
|
+
|
|
677
|
+
if (!this.tasks)
|
|
678
|
+
this.tasks = {};
|
|
679
|
+
|
|
680
|
+
Object.defineProperties(this.tasks, {
|
|
681
|
+
'_intervalTimerID': {
|
|
682
|
+
writable: true,
|
|
683
|
+
enumberable: false,
|
|
684
|
+
configurable: true,
|
|
685
|
+
value: setInterval(this.runTasks.bind(this), 1000),
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
if (flushTaskInfo !== false) {
|
|
690
|
+
this.taskInfo = { _startTime: nowInSeconds() };
|
|
691
|
+
} else {
|
|
692
|
+
this.taskInfo._startTime = nowInSeconds();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
iterateAllTaskInfos(callback) {
|
|
697
|
+
var tasks = this.tasks;
|
|
698
|
+
var allTasksInfo = this.taskInfo;
|
|
699
|
+
var taskNames = Object.keys(tasks);
|
|
700
|
+
|
|
701
|
+
for (var i = 0, il = taskNames.length; i < il; i++) {
|
|
702
|
+
var taskName = taskNames[i];
|
|
703
|
+
var tasksInfo = allTasksInfo[taskName];
|
|
704
|
+
if (!tasksInfo)
|
|
705
|
+
continue;
|
|
706
|
+
|
|
707
|
+
for (var j = 0, jl = tasksInfo.length; j < jl; j++) {
|
|
708
|
+
var taskInfo = tasksInfo[j];
|
|
709
|
+
callback(taskInfo, j, taskName);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
getAllTaskPromises() {
|
|
715
|
+
var promises = [];
|
|
716
|
+
|
|
717
|
+
this.iterateAllTaskInfos((taskInfo) => {
|
|
718
|
+
var promise = taskInfo.promise;
|
|
719
|
+
if (promise)
|
|
720
|
+
promises.push(promise);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
return promises;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async waitForAllTasksToFinish() {
|
|
727
|
+
// Request all tasks to stop
|
|
728
|
+
this.iterateAllTaskInfos((taskInfo) => (taskInfo.stop = true));
|
|
729
|
+
|
|
730
|
+
// Stop the tasks interval timer
|
|
731
|
+
this.stopTasks();
|
|
732
|
+
|
|
733
|
+
// Await on all running tasks to complete
|
|
734
|
+
var promises = this.getAllTaskPromises();
|
|
735
|
+
if (promises && promises.length) {
|
|
736
|
+
try {
|
|
737
|
+
await Promise.allSettled(promises);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
this.getLogger().error('Error while waiting for tasks to finish: ', error);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
promises = [];
|
|
744
|
+
|
|
745
|
+
this.iterateAllTaskInfos((taskInfo) => {
|
|
746
|
+
var taskInstance = taskInfo.taskInstance;
|
|
747
|
+
if (!taskInstance)
|
|
748
|
+
return;
|
|
749
|
+
|
|
750
|
+
promises.push(taskInstance.stop());
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
if (promises && promises.length) {
|
|
754
|
+
try {
|
|
755
|
+
await Promise.allSettled(promises);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
this.getLogger().error('Error while stopping tasks: ', error);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
createLogger(loggerOpts, Logger) {
|
|
763
|
+
return new Logger(loggerOpts);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
getLogger() {
|
|
767
|
+
if (!this.logger)
|
|
768
|
+
return console;
|
|
769
|
+
|
|
770
|
+
return this.logger;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async connectToDatabase(databaseConfig) {
|
|
774
|
+
if (!databaseConfig) {
|
|
775
|
+
this.getLogger().error(`Error: database connection options not defined`);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
var sequelize = new Sequelize(databaseConfig);
|
|
780
|
+
|
|
781
|
+
var dbConnectionString = `${databaseConfig.dialect}://${databaseConfig.host}:${databaseConfig.port || '<default port>'}/${databaseConfig.database}`;
|
|
782
|
+
try {
|
|
783
|
+
await sequelize.authenticate();
|
|
784
|
+
|
|
785
|
+
this.getLogger().log(`Connection to ${dbConnectionString} has been established successfully!`);
|
|
786
|
+
|
|
787
|
+
return sequelize;
|
|
788
|
+
} catch (error) {
|
|
789
|
+
this.getLogger().error(`Unable to connect to database ${dbConnectionString}:`, error);
|
|
790
|
+
await this.stop();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
getDBConnection() {
|
|
795
|
+
return this.dbConnection;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async createHTTPServer(options) {
|
|
799
|
+
var server = new HTTPServer(this, options);
|
|
800
|
+
|
|
801
|
+
await server.start();
|
|
802
|
+
|
|
803
|
+
return server;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async start() {
|
|
807
|
+
var options = this.getOptions();
|
|
808
|
+
|
|
809
|
+
var databaseConfig = this.getConfigValue('database.{environment}');
|
|
810
|
+
if (!databaseConfig)
|
|
811
|
+
databaseConfig = this.getConfigValue('database');
|
|
812
|
+
|
|
813
|
+
databaseConfig = Nife.extend(true, {}, databaseConfig || {}, options.database || {});
|
|
814
|
+
|
|
815
|
+
if (options.database !== false) {
|
|
816
|
+
if (Nife.isEmpty(databaseConfig)) {
|
|
817
|
+
this.getLogger().error(`Error: database connection for "${this.getConfigValue('environment')}" not defined`);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
databaseConfig.logging = this.getLogger().isDebugLevel();
|
|
822
|
+
|
|
823
|
+
if (Nife.isEmpty(databaseConfig.tablePrefix))
|
|
824
|
+
databaseConfig.tablePrefix = `${this.getApplicationName()}_`;
|
|
825
|
+
|
|
826
|
+
this.dbConnection = await this.connectToDatabase(databaseConfig);
|
|
827
|
+
|
|
828
|
+
this.setOptions({ database: databaseConfig });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (options.httpServer !== false) {
|
|
832
|
+
var httpServerConfig = this.getConfigValue('httpServer.{environment}');
|
|
833
|
+
if (!httpServerConfig)
|
|
834
|
+
httpServerConfig = this.getConfigValue('httpServer');
|
|
835
|
+
|
|
836
|
+
httpServerConfig = Nife.extend(true, {}, httpServerConfig || {}, options.httpServer || {});
|
|
837
|
+
if (Nife.isEmpty(httpServerConfig)) {
|
|
838
|
+
this.getLogger().error(`Error: httpServer options for "${this.getConfigValue('environment')}" not defined`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
this.server = await this.createHTTPServer(httpServerConfig);
|
|
843
|
+
|
|
844
|
+
this.setOptions({ httpServer: httpServerConfig });
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (options.database !== false) {
|
|
848
|
+
var models = await this.loadModels(options.modelsPath, databaseConfig);
|
|
849
|
+
this.models = models;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (options.httpServer !== false) {
|
|
853
|
+
var controllers = await this.loadControllers(options.controllersPath, this.server);
|
|
854
|
+
this.controllers = controllers;
|
|
855
|
+
|
|
856
|
+
var routes = await this.buildRoutes(this.server, this.getRoutes());
|
|
857
|
+
this.server.setRoutes(routes);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (options.runTasks !== false) {
|
|
861
|
+
var tasks = await this.loadTasks(options.tasksPath, databaseConfig);
|
|
862
|
+
this.tasks = tasks;
|
|
863
|
+
|
|
864
|
+
this.startTasks();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
await this.autoReload(options.autoReload, false);
|
|
868
|
+
|
|
869
|
+
this.isStarted = true;
|
|
870
|
+
|
|
871
|
+
this.emit('start');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async closeDBConnections() {
|
|
875
|
+
if (this.dbConnection) {
|
|
876
|
+
this.getLogger().info('Closing database connections...');
|
|
877
|
+
await this.dbConnection.close();
|
|
878
|
+
this.getLogger().info('All database connections closed successfully!');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async stopHTTPServer() {
|
|
883
|
+
if (this.server) {
|
|
884
|
+
this.getLogger().info('Stopping HTTP server...');
|
|
885
|
+
await this.server.stop();
|
|
886
|
+
this.getLogger().info('HTTP server stopped successfully!');
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async stopAllTasks() {
|
|
891
|
+
if (Nife.isNotEmpty(this.tasks) && this.tasks._intervalTimerID) {
|
|
892
|
+
this.getLogger().info('Waiting for all tasks to complete...');
|
|
893
|
+
await this.waitForAllTasksToFinish();
|
|
894
|
+
this.getLogger().info('All tasks completed!');
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async stop(exitCode) {
|
|
899
|
+
if (this.isStopping || !this.isStarted)
|
|
900
|
+
return;
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
this.getLogger().info('Shutting down...');
|
|
904
|
+
|
|
905
|
+
this.isStopping = true;
|
|
906
|
+
this.isStarted = false;
|
|
907
|
+
|
|
908
|
+
await this.autoReload(false, true);
|
|
909
|
+
|
|
910
|
+
await this.stopHTTPServer();
|
|
911
|
+
|
|
912
|
+
await this.stopAllTasks();
|
|
913
|
+
|
|
914
|
+
await this.closeDBConnections();
|
|
915
|
+
|
|
916
|
+
this.getLogger().info('Shut down complete!');
|
|
917
|
+
|
|
918
|
+
this.emit('stop');
|
|
919
|
+
|
|
920
|
+
var options = this.getOptions();
|
|
921
|
+
if (options.exitOnShutdown != null || exitCode != null) {
|
|
922
|
+
var code = (exitCode != null) ? exitCode : options.exitOnShutdown;
|
|
923
|
+
this.emit('exit', code);
|
|
924
|
+
process.exit(code);
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
this.getLogger().error('Error while shutting down: ', error);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
module.exports = {
|
|
933
|
+
Application,
|
|
934
|
+
};
|