/// import * as eta from "@eta/eta"; import * as fs from "fs-extra"; import * as orm from "typeorm"; import * as redis from "redis"; import ModuleLoader from "./ModuleLoader"; import WebServer from "./WebServer"; import * as db from "@eta/db"; Object.keys(db); // initializes models import * as promiseEvents from "promise-events"; export default class Application extends promiseEvents.EventEmitter { public server: WebServer; /** * Load module files */ public moduleLoaders: {[key: string]: ModuleLoader} = {}; public configs: {[key: string]: eta.Configuration} = {}; public controllers: {[key: string]: eta.HttpRoute} = {}; public staticFiles: {[key: string]: string} = {}; public viewFiles: {[key: string]: string} = {}; public viewMetadata: {[key: string]: {[key: string]: any}} = {}; public redis: redis.RedisClient; public async init(): Promise { await this.loadConfiguration(); process.env.eta_timezone = this.configs.global.get("server.timezone"); (eta).logger = new eta.StackLogger(__dirname + "/../logs"); this.server = new WebServer(); this.server.app = this; await this.loadModules(); await Promise.all(Object.keys(this.moduleLoaders).map(async k => eta.logger.stackDirs[k] = (await fs.realpath(this.moduleLoaders[k].baseDir)).replace(/\\/g, "/") )); eta.logger.generateStackKeys(); await this.emit("app:start"); eta.logger.info("Connecting to the database and initalizing ORM..."); await this.connectDatabases(); eta.logger.info("Successfully connected to the database."); try { this.redis = await this.connectRedis(); } catch (err) { eta.logger.error(err); return false; } eta.logger.info("Successfully connected to the Redis server."); await this.emit("database:connect"); return this.server.init(); } start(): void { this.server.start(); } close(): Promise { return this.server.close(); } private async loadConfiguration(): Promise { this.configs.root = await eta.Configuration.load(); const hosts: string[] = await fs.readdir(eta.constants.basePath + "/config"); hosts.forEach(h => this.configs[h] = this.configs.root.buildChild(["global.", h + "."])); delete this.configs.root; } private connectDatabases(): Promise { return Promise.all(Object.keys(this.configs).filter(k => k !== "global").map(k => { const config = this.configs[k]; const modelDirs = this.configs.global.modules() .map(m => this.configs.global.get(`modules.${m}.dirs.models`)) .reduce((p, v) => p.concat(v), []) .filter(d => d !== undefined) .map(d => d + "*.js"); const logOptions = config.get("logger.logDatabaseQueries") ? ["error", "query"] : []; return orm.createConnection(eta._.extend, orm.ConnectionOptions>({ entities: modelDirs, synchronize: !config.get("db.isReadOnly"), logging: logOptions, name: config.get("http.host"), namingStrategy: new eta.DatabaseNamingStrategy() }, config.buildToObject("db."))); })); } private connectRedis(): Promise { const tempClient: redis.RedisClient = redis.createClient( this.configs.global.get("session.port"), this.configs.global.get("session.host")); return new Promise((resolve, reject) => { tempClient.on("error", reject); tempClient.on("ready", () => resolve(tempClient)); }); } /** checks that the static file referenced by mvcPath actually exists in filesystem */ async verifyStaticFile(path: string): Promise { if (this.staticFiles[path]) { const exists: boolean = await fs.pathExists(this.staticFiles[path]); if (!exists) { delete this.staticFiles[path]; } return exists; } const staticDirs: string[] = this.configs.global.modules() .map(m => this.configs.global.get(`modules.${m}.dirs.staticFiles`) || []) .reduce((p, v) => p.concat(v), []); for (const staticDir of staticDirs) { if (await fs.pathExists(staticDir + path) && (await fs.lstat(staticDir + path)).isFile()) { this.staticFiles[path] = staticDir + path; return true; } } return false; } getActionsWithFlag(flag: string, context: eta.RequestHandler, flagValue?: string | boolean | number): { flagValue: any; action(...args: any[]): Promise; }[] { context = context || { server: this.server }; return Object.values(this.controllers).map(route => route.actions.filter(a => !!a[flag] && (flagValue === undefined || a[flag] === flagValue)).map(a => ({ flagValue: a[flag], action: (...args: any[]) => { const instance: eta.HttpController = new route.controller(context); return (instance)[a.name].bind(instance)(...args); } })) ).reduce((p, v) => p.concat(v), []); } private async loadModules(): Promise { const moduleDirs: string[] = await fs.readdir(eta.constants.modulesPath); eta.logger.info(`Found ${moduleDirs.length} modules: ${moduleDirs.join(", ")}`); for (const moduleName of moduleDirs) { this.moduleLoaders[moduleName] = new ModuleLoader(moduleName, this); this.moduleLoaders[moduleName].on("controller-load", (route: eta.HttpRoute) => { delete(this.controllers[route.route]); this.controllers[route.route] = route; this.emit("load:controller", route).catch(err => eta.logger.error(err)); }).on("metadata-load", (mvcPath: string) => { this.viewMetadata[mvcPath] = this.moduleLoaders[moduleName].viewMetadata[mvcPath]; if (this.moduleLoaders[moduleName].isInitialized) { const includePaths: string[] = (this.viewMetadata[mvcPath].include || []); for (const includePath of includePaths) { this.viewMetadata[mvcPath] = eta._.merge(this.viewMetadata[includePath] || {}, this.viewMetadata[mvcPath]); } } this.emit("load:view-metadata").catch(err => eta.logger.error(err)); }); await this.moduleLoaders[moduleName].loadAll(); if (!this.moduleLoaders[moduleName].isInitialized) { delete this.moduleLoaders[moduleName]; } } const lifecycleHandlers: (new (app: Application) => eta.LifecycleHandler)[] = []; // map all modules' objects into webserver's global arrays Object.keys(this.moduleLoaders).sort().forEach(k => { const moduleLoader: ModuleLoader = this.moduleLoaders[k]; lifecycleHandlers.push(...moduleLoader.lifecycleHandlers); this.staticFiles = eta._.defaults(moduleLoader.staticFiles, this.staticFiles); this.viewFiles = eta._.defaults(moduleLoader.viewFiles, this.viewFiles); }); lifecycleHandlers.forEach(LifecycleHandler => new LifecycleHandler(this).register()); let unvisitedPaths: string[] = Object.keys(this.viewMetadata).filter(k => (this.viewMetadata[k].include || []).length === 0); const nextPaths: string[] = []; while (unvisitedPaths.length > 0) { const currentPath = unvisitedPaths.splice(0, 1)[0]; const otherPaths = Object.keys(this.viewMetadata).filter(k => (this.viewMetadata[k].include || []).includes(currentPath)); for (const otherPath of otherPaths) { this.viewMetadata[otherPath] = eta._.mergeWith(this.viewMetadata[otherPath], this.viewMetadata[currentPath], (objValue: any, srcValue: any) => { if (!objValue || !srcValue || objValue.constructor.name !== srcValue.constructor.name || objValue.constructor.name !== "Array") return undefined; return srcValue.concat(objValue); } ); nextPaths.push(otherPath); } if (unvisitedPaths.length === 0) unvisitedPaths = eta._.uniq(nextPaths.splice(0, nextPaths.length)); } } }