UNPKG

27.6 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11var __generator = (this && this.__generator) || function (thisArg, body) {
12 var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13 return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14 function verb(n) { return function (v) { return step([n, v]); }; }
15 function step(op) {
16 if (f) throw new TypeError("Generator is already executing.");
17 while (_) try {
18 if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19 if (y = 0, t) op = [op[0] & 2, t.value];
20 switch (op[0]) {
21 case 0: case 1: t = op; break;
22 case 4: _.label++; return { value: op[1], done: false };
23 case 5: _.label++; y = op[1]; op = [0]; continue;
24 case 7: op = _.ops.pop(); _.trys.pop(); continue;
25 default:
26 if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27 if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28 if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29 if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30 if (t[2]) _.ops.pop();
31 _.trys.pop(); continue;
32 }
33 op = body.call(thisArg, _);
34 } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35 if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36 }
37};
38Object.defineProperty(exports, "__esModule", { value: true });
39exports.app = exports.getLanguageFromRequest = exports.isProduction = void 0;
40var express = require("express");
41var http = require("http");
42var nunjucks = require("nunjucks");
43var fs = require("fs");
44require("reflect-metadata");
45var typeorm_1 = require("typeorm");
46var session = require("express-session");
47var bodyParser = require("body-parser");
48var multer = require("multer");
49var cors = require("cors");
50var moment = require("moment");
51var flash = require("express-flash");
52var apollo_server_express_1 = require("apollo-server-express");
53var migration_entity_1 = require("./entities/migration.entity");
54var error_controller_1 = require("./error.controller");
55var expressGenerator = require("./express-generator");
56var graphqlGenerator = require("./graphql/generator");
57var setup_1 = require("./entities/setup");
58var jsonwebtoken_1 = require("jsonwebtoken");
59var translations = {};
60var routes = {};
61var logger_1 = require("./logger");
62var api_response_wrapper_1 = require("./api-response-wrapper");
63/**
64 * Utility function to check if we are in the production environment.
65 * @return true if the NODE_ENV is set to "production", false otherwise
66 */
67function isProduction() {
68 return process.env.NODE_ENV === "production";
69}
70exports.isProduction = isProduction;
71/**
72 * Retrieve the preferred language from an express request, using the accepts-languages
73 * header value.
74 * @param req the express request
75 * @return the two letter lower-cased language, or "*" (wildcard), or null
76 */
77function getLanguageFromRequest(req) {
78 var lang = req.acceptsLanguages()[0];
79 if (lang.indexOf("-") !== -1) {
80 lang = lang.split("-")[0];
81 }
82 if (lang) {
83 lang = lang.trim().toLowerCase();
84 }
85 return lang;
86}
87exports.getLanguageFromRequest = getLanguageFromRequest;
88/**
89 * This function shall be called with the nunjucks environment as self parameter!
90 * It retrieve the language of the current request, using the default
91 * language set in the app as fallback.
92 */
93function retrieveLanguage(self) {
94 var lang = null;
95 try {
96 var req = self.ctx.req;
97 lang = getLanguageFromRequest(req);
98 if (lang === "*") {
99 lang = null;
100 }
101 }
102 catch (e) { }
103 if (!lang) {
104 lang = self.getVariables()["lang"];
105 }
106 if (!lang) {
107 var app_1 = self.ctx.req.app.get("app");
108 lang = app_1.config.defaultLanguage;
109 }
110 return lang;
111}
112/**
113 * Implementation of the tr filer for the nunjucks engine.
114 * It trying to understand the current language from the request. The fallback
115 * uses the defaultLanguage set on the app.
116 */
117function translate(str) {
118 try {
119 var lang = retrieveLanguage(this);
120 return performTranslation(str, translations[lang]);
121 }
122 catch (e) {
123 logger_1.logger.info(e);
124 logger_1.logger.info(this);
125 }
126 return str;
127}
128function performTranslation(str, translations) {
129 var translation = translations[str];
130 if (translation) {
131 return translation;
132 }
133 var start = str.indexOf("{{");
134 var end = str.indexOf("}}");
135 if (start != -1 && end != -1) {
136 var key = str.substring(start + 2, end);
137 translation = translations[key.trim()];
138 return str.replace("{{" + key + "}}", translation);
139 }
140 return str;
141}
142/**
143 * Implementation of the date filter using moment.
144 * The default implementation uses the "lll" string format, resulting in
145 * Feb 19, 2018 4:57 PM in English.
146 * @param d the date to format
147 * @param format the string to format the date, default to lll
148 * @return the formatted date
149 */
150function date(d, format) {
151 var lang = retrieveLanguage(this);
152 var m = moment(d).locale(lang);
153 if (!format) {
154 format = "lll";
155 }
156 return m.format(format);
157}
158/**
159 * Apply parameters to an URL. If a parameter is not found as path parameter,
160 * it is added as query parameter.
161 * @param url the url to compile
162 * @param parameters a plain object containing the parameters
163 * @return the compiled url
164 */
165function applyParametersToUrl(url, parameters) {
166 if (!parameters) {
167 return url;
168 }
169 if (url.indexOf("?") == -1) {
170 url += "?";
171 }
172 else {
173 url += "&";
174 }
175 for (var key in parameters) {
176 if (url.indexOf(":" + key) == -1) {
177 url += key + "=" + parameters[key] + "&";
178 }
179 else {
180 url = url.replace(":" + key, parameters[key]);
181 }
182 }
183 if (url.endsWith("?") || url.endsWith("&")) {
184 url = url.substring(0, url.length - 1);
185 }
186 return url;
187}
188/**
189 * Transform the route name to a URL and compile it with the given parameters.
190 * @param name the route name (or, eventually, the path)
191 * @param parameters a plain object containing the parameters
192 * @return the compiled url
193 */
194function route(name, parameters) {
195 var url = name;
196 if (routes[name]) {
197 url = routes[name];
198 }
199 return applyParametersToUrl(url, parameters);
200}
201/**
202 * Implementation of the old filter function. This function returns the previous
203 * value of the input form. Fallback to the defaultValue.
204 * @param name the name used in the form
205 * @param defaultValue a fallback value
206 * @return the previous value or defaultValue
207 */
208function old(name, defaultValue) {
209 var req = this.ctx.req;
210 if (req.body && req.body[name]) {
211 return req.body[name];
212 }
213 if (req.query[name]) {
214 return req.query[name];
215 }
216 return defaultValue;
217}
218/**
219 * Implementation of the format filter function to format a float number.
220 * By default, the number is formatted with 2 decimal numbers.
221 * @param val the number to format
222 * @param decimal the number of decimal number
223 * @return the formatted number as a string
224 */
225function format(val, decimal) {
226 if (decimal === void 0) { decimal = 2; }
227 return Number(val).toFixed(decimal);
228}
229/**
230 * Implementation of the resolvePath global function. Using this function, it is
231 * possible to refer to any views with a virtual folder containing all the available
232 * views.
233 * @param path the virtual absolute path of the view
234 * @return the absolute path of the view if resolved, or the original path otherwise
235 */
236function resolvePath(path) {
237 var normalizedPath = path;
238 if (normalizedPath.endsWith(".njk")) {
239 normalizedPath = normalizedPath.substring(0, normalizedPath.length - 4);
240 }
241 var app = this.ctx.req.app.get("app");
242 var resolved = app.templateMap[path];
243 if (resolved) {
244 return resolved;
245 }
246 return path;
247}
248/**
249 * This function returns the current server url, with the used protocol and port.
250 * This function is available as global function on nunjucks.
251 */
252function currentHost() {
253 var req = this.ctx.req;
254 return req.protocol + '://' + req.get('host');
255}
256/**
257 * The App class contains the initialization code for a Lynx application.
258 */
259var App = /** @class */ (function () {
260 function App(config, modules) {
261 var _this = this;
262 this._modules = new Set();
263 this.apiResponseWrapper = new api_response_wrapper_1.DefaultAPIResponseWrapper();
264 this._config = config;
265 exports.app = this;
266 if (modules) {
267 this._modules = new Set(modules);
268 this._modules.forEach(function (module) { return module.mount(_this._config); });
269 }
270 config.db.entities.unshift(__dirname + "/entities/*.entity.js");
271 config.middlewaresFolders.unshift(__dirname + "/middlewares");
272 config.viewFolders.unshift(__dirname + "/views");
273 if (!config.disabledDb) {
274 typeorm_1.createConnection(config.db)
275 .then(function (_) {
276 // here you can start to work with your entities
277 logger_1.logger.info("Connection to the db established!");
278 setup_1.setup(config.db.entities)
279 .then(function (_) {
280 _this._modules.forEach(function (module) {
281 return module.onDatabaseConnected();
282 });
283 if (!config.disableMigrations) {
284 _this.executeMigrations()
285 .catch(function (err) {
286 logger_1.logger.error(err);
287 process.exit(1);
288 })
289 .then(function () {
290 if (_this._config.onDatabaseInit) {
291 _this._config.onDatabaseInit();
292 }
293 });
294 }
295 else if (_this._config.onDatabaseInit) {
296 _this._config.onDatabaseInit();
297 }
298 })
299 .catch(function (error) {
300 logger_1.logger.error(error);
301 process.exit(1);
302 });
303 })
304 .catch(function (error) {
305 logger_1.logger.error(error);
306 process.exit(1);
307 });
308 }
309 else {
310 logger_1.logger.debug("The DB service is disabled");
311 }
312 this.express = express();
313 this.httpServer = http.createServer(this.express);
314 this.express.set("app", this);
315 this.express.use(function (_, res, next) {
316 res.setHeader('X-Powered-By', 'lynx-framework/express');
317 next();
318 });
319 this.express.use("/api/*", cors());
320 if (this.config.jsonLimit) {
321 this.express.use(bodyParser.json({ limit: this.config.jsonLimit }));
322 }
323 else {
324 this.express.use(bodyParser.json());
325 }
326 this.express.use(bodyParser.urlencoded({ extended: true }));
327 var app_session_options = {
328 secret: config.sessionSecret,
329 resave: false,
330 saveUninitialized: true
331 };
332 if (config.sessionStore) {
333 app_session_options.store = config.sessionStore;
334 }
335 var app_session = session(app_session_options);
336 this.express.use(app_session);
337 this.express.use(flash());
338 this._upload = multer({ dest: config.uploadPath });
339 fs.exists(config.cachePath, function (exists) {
340 if (!exists) {
341 fs.mkdir(config.cachePath, function (err) {
342 if (err) {
343 logger_1.logger.error("Error creating the local cache directory", err);
344 }
345 });
346 }
347 });
348 for (var _i = 0, _a = config.publicFolders; _i < _a.length; _i++) {
349 var folder = _a[_i];
350 this.express.use(express.static(folder));
351 }
352 this.generateTemplateMap(config.viewFolders);
353 this._nunjucksEnvironment = nunjucks.configure(config.viewFolders, {
354 autoescape: true,
355 watch: true,
356 express: this.express
357 });
358 this._nunjucksEnvironment.addFilter("tr", translate);
359 this._nunjucksEnvironment.addFilter("json", JSON.stringify);
360 this._nunjucksEnvironment.addFilter("format", format);
361 this._nunjucksEnvironment.addFilter("date", date);
362 this.loadTranslations(config.translationFolders);
363 this._nunjucksEnvironment.addGlobal("route", route);
364 this._nunjucksEnvironment.addGlobal("old", old);
365 this._nunjucksEnvironment.addGlobal("resolvePath", resolvePath);
366 this._nunjucksEnvironment.addGlobal("currentHost", currentHost);
367 for (var _b = 0, _c = config.middlewaresFolders; _b < _c.length; _b++) {
368 var path = _c[_b];
369 this.loadMiddlewares(path);
370 }
371 for (var _d = 0, _e = config.controllersFolders; _d < _e.length; _d++) {
372 var path = _e[_d];
373 this.loadControllers(path);
374 }
375 if (!config.disabledDb && !config.disabledGraphQL) {
376 var schema = graphqlGenerator.generateSchema(config.db.entities);
377 // The GraphQL endpoint
378 this.express.use("/graphql", bodyParser.json(), apollo_server_express_1.graphqlExpress({ schema: schema }));
379 // GraphiQL, a visual editor for queries
380 this.express.use("/graphiql", apollo_server_express_1.graphiqlExpress({ endpointURL: "/graphql" }));
381 }
382 this._errorController = new error_controller_1.default(this);
383 }
384 Object.defineProperty(App.prototype, "config", {
385 get: function () {
386 return this._config;
387 },
388 enumerable: false,
389 configurable: true
390 });
391 Object.defineProperty(App.prototype, "templateMap", {
392 get: function () {
393 return this._templateMap;
394 },
395 enumerable: false,
396 configurable: true
397 });
398 Object.defineProperty(App.prototype, "nunjucksEnvironment", {
399 get: function () {
400 return this._nunjucksEnvironment;
401 },
402 enumerable: false,
403 configurable: true
404 });
405 Object.defineProperty(App.prototype, "upload", {
406 get: function () {
407 return this._upload;
408 },
409 enumerable: false,
410 configurable: true
411 });
412 Object.defineProperty(App.prototype, "customErrorController", {
413 /**
414 * This property allow the customization of the standard error controller.
415 * You need to create the controller using its standard constructor:
416 * new MyCustomErrorController(app)
417 */
418 set: function (ctrl) {
419 this._errorController = ctrl;
420 },
421 enumerable: false,
422 configurable: true
423 });
424 App.prototype.recursiveGenerateTemplateMap = function (path, currentPath) {
425 var files = fs.readdirSync(path);
426 for (var index in files) {
427 var currentFilePath = path + "/" + files[index];
428 if (fs.lstatSync(currentFilePath).isDirectory()) {
429 this.recursiveGenerateTemplateMap(currentFilePath, currentPath + files[index] + "/");
430 continue;
431 }
432 var name = files[index].replace(".njk", "");
433 this._templateMap[currentPath + name] = currentFilePath;
434 }
435 };
436 App.prototype.generateTemplateMap = function (paths) {
437 this._templateMap = {};
438 for (var _i = 0, paths_1 = paths; _i < paths_1.length; _i++) {
439 var path = paths_1[_i];
440 this.recursiveGenerateTemplateMap(path, "/");
441 }
442 };
443 App.prototype.recursiveExecuteMigrations = function (path) {
444 return __awaiter(this, void 0, void 0, function () {
445 var files, _a, _b, _i, index, currentFilePath, m, entity, migration, e_1;
446 return __generator(this, function (_c) {
447 switch (_c.label) {
448 case 0:
449 if (!fs.existsSync(path)) {
450 logger_1.logger.warn("The migration folder " + path + " doesn't exists!");
451 return [2 /*return*/];
452 }
453 files = fs.readdirSync(path).sort(function (a, b) { return a.localeCompare(b); });
454 _a = [];
455 for (_b in files)
456 _a.push(_b);
457 _i = 0;
458 _c.label = 1;
459 case 1:
460 if (!(_i < _a.length)) return [3 /*break*/, 13];
461 index = _a[_i];
462 currentFilePath = path + "/" + files[index];
463 if (!fs.lstatSync(currentFilePath).isDirectory()) return [3 /*break*/, 3];
464 return [4 /*yield*/, this.recursiveExecuteMigrations(currentFilePath)];
465 case 2:
466 _c.sent();
467 return [3 /*break*/, 12];
468 case 3:
469 if (currentFilePath.endsWith("ts"))
470 return [3 /*break*/, 12];
471 m = require(currentFilePath);
472 if (!m.default) {
473 throw new Error("Please define the migration as the export default class in file " +
474 currentFilePath +
475 ".");
476 }
477 return [4 /*yield*/, migration_entity_1.default.findByName(currentFilePath)];
478 case 4:
479 entity = _c.sent();
480 if (entity && entity.wasExecuted()) {
481 return [3 /*break*/, 12];
482 }
483 if (!!entity) return [3 /*break*/, 6];
484 entity = new migration_entity_1.default();
485 entity.name = currentFilePath;
486 return [4 /*yield*/, entity.save()];
487 case 5:
488 _c.sent();
489 _c.label = 6;
490 case 6:
491 migration = new m.default();
492 _c.label = 7;
493 case 7:
494 _c.trys.push([7, 10, , 12]);
495 return [4 /*yield*/, migration.up()];
496 case 8:
497 _c.sent();
498 entity.setExecuted();
499 return [4 /*yield*/, entity.save()];
500 case 9:
501 _c.sent();
502 logger_1.logger.info("Migration " + currentFilePath + " executed!");
503 return [3 /*break*/, 12];
504 case 10:
505 e_1 = _c.sent();
506 entity.setFailed();
507 return [4 /*yield*/, entity.save()];
508 case 11:
509 _c.sent();
510 logger_1.logger.error("Error executing the migration " + currentFilePath);
511 throw e_1;
512 case 12:
513 _i++;
514 return [3 /*break*/, 1];
515 case 13: return [2 /*return*/];
516 }
517 });
518 });
519 };
520 /**
521 * This method will execute the migrations.
522 * By default, this method will be executed automatically during the app
523 * startup. In some scenario, like hight-scalability, this behaviour could
524 * be unwanted. Thus, it is possibly otherwise to explicitly call this method
525 * in some other way (for example, connecting it to a standard http route).
526 */
527 App.prototype.executeMigrations = function () {
528 return __awaiter(this, void 0, void 0, function () {
529 var _i, _a, path;
530 return __generator(this, function (_b) {
531 switch (_b.label) {
532 case 0:
533 _i = 0, _a = this._config.migrationsFolders;
534 _b.label = 1;
535 case 1:
536 if (!(_i < _a.length)) return [3 /*break*/, 4];
537 path = _a[_i];
538 return [4 /*yield*/, this.recursiveExecuteMigrations(path)];
539 case 2:
540 _b.sent();
541 _b.label = 3;
542 case 3:
543 _i++;
544 return [3 /*break*/, 1];
545 case 4: return [2 /*return*/];
546 }
547 });
548 });
549 };
550 App.prototype.loadTranslations = function (paths) {
551 for (var _i = 0, paths_2 = paths; _i < paths_2.length; _i++) {
552 var path = paths_2[_i];
553 var files = fs.readdirSync(path);
554 for (var index in files) {
555 var nameWithExtension = files[index];
556 if (!nameWithExtension.endsWith("json"))
557 continue;
558 var name = nameWithExtension.substring(0, nameWithExtension.indexOf("."));
559 var tmp = JSON.parse(fs.readFileSync(path + "/" + nameWithExtension, "utf8"));
560 if (!translations[name]) {
561 translations[name] = {};
562 }
563 for (var key in tmp) {
564 translations[name][key] = tmp[key];
565 }
566 }
567 }
568 };
569 App.prototype.loadMiddlewares = function (path) {
570 if (!fs.existsSync(path)) {
571 logger_1.logger.warn("The middleares folder " + path + " doesn't exists!");
572 return;
573 }
574 var middlewares = fs.readdirSync(path);
575 for (var index in middlewares) {
576 var currentFilePath = path + "/" + middlewares[index];
577 if (fs.lstatSync(currentFilePath).isDirectory()) {
578 this.loadMiddlewares(currentFilePath);
579 continue;
580 }
581 if (middlewares[index].endsWith("ts"))
582 continue;
583 var midd = require(currentFilePath);
584 if (!midd.default) {
585 throw new Error("Please define the middleware as the export default class in file " +
586 currentFilePath +
587 ".");
588 }
589 expressGenerator.useMiddleware(this, midd.default);
590 }
591 };
592 App.prototype.loadControllers = function (path) {
593 var files = fs.readdirSync(path);
594 for (var index in files) {
595 var currentFilePath = path + "/" + files[index];
596 if (fs.lstatSync(currentFilePath).isDirectory()) {
597 this.loadControllers(currentFilePath);
598 continue;
599 }
600 if (files[index].endsWith("ts"))
601 continue;
602 var ctrl = require(currentFilePath);
603 if (!ctrl.default) {
604 throw new Error("Please define the controller as the export default class in file " +
605 currentFilePath +
606 ".");
607 }
608 expressGenerator.useController(this, ctrl.default, routes);
609 }
610 };
611 App.prototype.startServer = function (port) {
612 var _this = this;
613 this.express.use(function (req, res) {
614 _this._errorController
615 .onNotFound(req)
616 .then(function (r) {
617 if (!res.headersSent) {
618 res.status(404);
619 }
620 r.performResponse(req, res);
621 })
622 .catch(function (err) {
623 if (!res.headersSent) {
624 res.status(404);
625 }
626 res.send(err);
627 });
628 });
629 this.express.use(function (error, req, res, _) {
630 _this._errorController
631 .onError(error, req)
632 .then(function (r) {
633 if (!res.headersSent) {
634 res.status(500);
635 }
636 r.performResponse(req, res);
637 })
638 .catch(function (err) {
639 if (!res.headersSent) {
640 res.status(500);
641 }
642 res.send(err);
643 });
644 });
645 this.httpServer.listen(port, function () {
646 logger_1.logger.info("server is listening on " + port);
647 });
648 };
649 App.prototype.route = function (name, parameters) {
650 return route(name, parameters);
651 };
652 App.prototype.translate = function (str, req) {
653 try {
654 var lang = getLanguageFromRequest(req);
655 if (!lang) {
656 lang = this._config.defaultLanguage;
657 }
658 return performTranslation(str, translations[lang]);
659 }
660 catch (e) {
661 logger_1.logger.info(e);
662 }
663 return str;
664 };
665 App.prototype.generateTokenForUser = function (user) {
666 return jsonwebtoken_1.sign({ id: user.id }, this._config.tokenSecret, {
667 expiresIn: "1y"
668 });
669 };
670 return App;
671}());
672exports.default = App;
673Object.defineProperty(Array.prototype, "serialize", {
674 enumerable: false,
675 configurable: true,
676 value: function () {
677 var r = [];
678 for (var _i = 0, _a = this; _i < _a.length; _i++) {
679 var el = _a[_i];
680 if (el.serialize) {
681 r.push(el.serialize());
682 }
683 else {
684 r.push(el);
685 }
686 }
687 return r;
688 }
689});
690Object.defineProperty(Array.prototype, "addHiddenField", {
691 enumerable: false,
692 configurable: true,
693 value: function (field) {
694 for (var _i = 0, _a = this; _i < _a.length; _i++) {
695 var el = _a[_i];
696 if (el.addHiddenField) {
697 el.addHiddenField(field);
698 }
699 }
700 }
701});
702Object.defineProperty(Array.prototype, "removeHiddenField", {
703 enumerable: false,
704 configurable: true,
705 value: function (field) {
706 for (var _i = 0, _a = this; _i < _a.length; _i++) {
707 var el = _a[_i];
708 if (el.removeHiddenField) {
709 el.removeHiddenField(field);
710 }
711 }
712 }
713});