diff --git a/package-lock.json b/package-lock.json index 7e6a5f0..b1b740e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "@athenna/core", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@athenna/core", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "dependencies": { "@athenna/config": "1.0.5", - "@athenna/http": "1.1.1", + "@athenna/http": "1.1.3", "@athenna/ioc": "1.0.9", - "@athenna/logger": "1.0.7", + "@athenna/logger": "1.0.9", "@secjs/utils": "1.8.0", "reflect-metadata": "0.1.13", "tscpaths": "0.0.9" @@ -71,9 +71,9 @@ } }, "node_modules/@athenna/http": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@athenna/http/-/http-1.1.1.tgz", - "integrity": "sha512-5+aOvBLvKGJexVJK7saGYZIWRVH5S4wyyeB+6G4Mf067TxIhbU49YIHrqcUqxYrYgkD6oxEN0JOAFBizhJbi0w==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@athenna/http/-/http-1.1.3.tgz", + "integrity": "sha512-p5i7dxDzha9HRBRQH8IHFkA4wN+1fj3LMymzmaf6gc5c0GHQ+luN69CbDSqlOKqv7emw4+W7jD62M4ol/I1LcA==", "dependencies": { "@athenna/config": "1.0.5", "@athenna/ioc": "1.0.9", @@ -201,16 +201,81 @@ } }, "node_modules/@athenna/logger": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@athenna/logger/-/logger-1.0.7.tgz", - "integrity": "sha512-x0U4cLVDDdZSfr5daPWgf/Riv2XiZQhD7CMpSEMdKd4Jex5X3YROqCgwt61bvG98GFQtD0bwSZEjpwUYThPCfw==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@athenna/logger/-/logger-1.0.9.tgz", + "integrity": "sha512-qJa0VSeKWjLrZ9i0roa3MC2abem8whYA0i2XPHbUZ0WP7mvwCRT2EJZZex1Jv4nB+wLZy4M6f5bFmwOwztksXQ==", "dependencies": { "@athenna/ioc": "1.0.9", "@secjs/utils": "1.8.0", + "chalk": "4.1.1", "reflect-metadata": "0.1.13", "tscpaths": "0.0.9" } }, + "node_modules/@athenna/logger/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@athenna/logger/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@athenna/logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@athenna/logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@athenna/logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@athenna/logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -13505,9 +13570,9 @@ } }, "@athenna/http": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@athenna/http/-/http-1.1.1.tgz", - "integrity": "sha512-5+aOvBLvKGJexVJK7saGYZIWRVH5S4wyyeB+6G4Mf067TxIhbU49YIHrqcUqxYrYgkD6oxEN0JOAFBizhJbi0w==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@athenna/http/-/http-1.1.3.tgz", + "integrity": "sha512-p5i7dxDzha9HRBRQH8IHFkA4wN+1fj3LMymzmaf6gc5c0GHQ+luN69CbDSqlOKqv7emw4+W7jD62M4ol/I1LcA==", "requires": { "@athenna/config": "1.0.5", "@athenna/ioc": "1.0.9", @@ -13610,14 +13675,60 @@ } }, "@athenna/logger": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@athenna/logger/-/logger-1.0.7.tgz", - "integrity": "sha512-x0U4cLVDDdZSfr5daPWgf/Riv2XiZQhD7CMpSEMdKd4Jex5X3YROqCgwt61bvG98GFQtD0bwSZEjpwUYThPCfw==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@athenna/logger/-/logger-1.0.9.tgz", + "integrity": "sha512-qJa0VSeKWjLrZ9i0roa3MC2abem8whYA0i2XPHbUZ0WP7mvwCRT2EJZZex1Jv4nB+wLZy4M6f5bFmwOwztksXQ==", "requires": { "@athenna/ioc": "1.0.9", "@secjs/utils": "1.8.0", + "chalk": "4.1.1", "reflect-metadata": "0.1.13", "tscpaths": "0.0.9" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } } }, "@babel/code-frame": { diff --git a/package.json b/package.json index 202cd15..1026a45 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/core", - "version": "1.1.2", + "version": "1.1.3", "description": "", "license": "MIT", "author": "João Lenon ", @@ -9,8 +9,8 @@ "homepage": "https://github.com/AthennaIO/Core#readme", "scripts": { "build": "tsc --project tsconfig.json && tscpaths -p tsconfig.json -s . -o .", - "test": "npm run lint:fix && cross-env NODE_TS=true jest --verbose", - "test:debug": "DEBUG=api:* && npm run test", + "test": "npm run lint:fix && cross-env CORE_TESTING=true cross-env NODE_TS=true jest --verbose", + "test:debug": "cross-env DEBUG=api:* && npm run test", "lint:fix": "eslint \"{src,container,tests}/**/*.ts\" --fix" }, "keywords": [ @@ -164,8 +164,8 @@ "dependencies": { "@athenna/config": "1.0.5", "@athenna/ioc": "1.0.9", - "@athenna/logger": "1.0.7", - "@athenna/http": "1.1.1", + "@athenna/logger": "1.0.9", + "@athenna/http": "1.1.3", "@secjs/utils": "1.8.0", "reflect-metadata": "0.1.13", "tscpaths": "0.0.9" diff --git a/src/Application.ts b/src/Application.ts new file mode 100644 index 0000000..aeb20b5 --- /dev/null +++ b/src/Application.ts @@ -0,0 +1,221 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { parse } from 'path' +import { Ioc } from '@athenna/ioc' +import { Path } from '@secjs/utils' +import { Logger } from 'src/Utils/Logger' +import { Http, Router as HttpRoute } from '@athenna/http' +import { ResolveClassExport } from 'src/Utils/ResolveClassExport' +import { AthennaErrorHandler } from 'src/Utils/AthennaErrorHandler' +import { NotBootedException } from 'src/Exceptions/NotBootedException' +import { AlreadyBootedException } from 'src/Exceptions/AlreadyBootedException' +import { AlreadyShutdownException } from 'src/Exceptions/AlreadyShutdownException' + +export class Application { + /** + * An instance of the Ioc class that is a Monostate with + * the Awilix container inside. + * + * @private + */ + private readonly container: Ioc + + /** + * An instance of the application. Is here that the + * client will bootstrap his type of application. + * + * @private + */ + private readonly extension: string + private httpServer: Http | null + private httpRoute: HttpRoute | null + + public constructor(extension: string) { + this.container = new Ioc() + this.httpServer = null + this.httpRoute = null + this.extension = extension + } + + /** + * An instance of the Ioc class that is a Monostate with + * the Awilix container inside. + * + * @return Ioc + */ + getContainer(): Ioc { + return this.container + } + + /** + * An instance of the Http class or an exception if it's not + * booted. + * + * @return Http + */ + getHttpServer(): Http { + if (!this.httpServer) { + throw new NotBootedException('HttpServer') + } + + return this.httpServer + } + + /** + * An instance of the Router class or an exception if it's not + * booted. + * + * @return HttpRoute + */ + getHttpRoute(): HttpRoute { + if (!this.httpRoute) { + throw new NotBootedException('HttpServer') + } + + return this.httpRoute + } + + // TODO + // async bootWorker() {} + + // TODO + // async shutdownWorker() {} + + // TODO + // async bootConsole() {} + + // TODO + // async shutdownConsole() {} + + /** + * Boot a new HttpServer inside this Application instance. + * + * @return void + */ + async bootHttpServer(): Promise { + if (this.httpServer) { + throw new AlreadyBootedException('HttpServer') + } + + this.httpServer = this.container.safeUse('Athenna/Core/HttpServer') + this.httpRoute = this.container.safeUse('Athenna/Core/HttpRoute') + + /** + * Resolve the Kernel file inside Http directory. It's + * extremely important to call this method before preloading + * the routes/http file because of named middlewares + */ + this.resolveHttpKernel() + + /** + * Preload default http route file + */ + this.preloadFile(Path.pwd('routes/http')) + + this.httpServer.setErrorHandler(AthennaErrorHandler.http) + this.httpRoute.register() + + const port = Config.get('http.port') + const host = Config.get('http.host') + + await this.httpServer.listen(port, host) + + Logger.log(`Http server started on http://${host}:${port}`) + + return this.httpServer + } + + /** + * Shutdown the HttpServer inside this Application instance. + * + * @return void + */ + async shutdownHttpServer() { + if (!this.httpServer) { + throw new AlreadyShutdownException('HttpServer') + } + + await this.httpServer.close() + + this.httpRoute = null + this.httpServer = null + } + + /** + * Preload the file according to filePath. This is usefully + * to preload default files of each type of application. Such + * as routes and Kernels + * + * @param filePath + * @private + */ + private preloadFile(filePath: string) { + const { dir, name } = parse(filePath) + + Logger.log(`Preloading ${name} file`) + + require(`${dir}/${name}${this.extension}`) + } + + /** + * Resolve the Kernel of the http server + * + * @private + */ + private resolveHttpKernel() { + const { dir, name } = parse(Path.app('Http/Kernel')) + + const HttpKernel = ResolveClassExport.resolve( + require(`${dir}/${name}${this.extension}`), + ) + + const httpKernel = new HttpKernel() + const container = this.getContainer() + const httpServer = this.getHttpServer() + + /** + * Binding the named middlewares inside the container and + * creating a simple alias for it. + */ + Object.keys(httpKernel.namedMiddlewares).forEach(key => { + const Middleware = ResolveClassExport.resolve( + httpKernel.namedMiddlewares[key], + ) + + if (!container.hasDependency(`App/Middlewares/${Middleware.name}`)) { + container.bind(`App/Middlewares/${Middleware.name}`, Middleware) + } + + container.alias( + `App/Middlewares/Names/${key}`, + `App/Middlewares/${Middleware.name}`, + ) + }) + + /** + * Resolving global middlewares inside the Http server. + */ + httpKernel.globalMiddlewares.forEach(globalMiddleware => { + globalMiddleware = ResolveClassExport.resolve(globalMiddleware) + + if (globalMiddleware.handle) { + httpServer.use(globalMiddleware.handle, 'handle') + } + + if (globalMiddleware.intercept) { + httpServer.use(globalMiddleware.intercept, 'intercept') + } + + if (globalMiddleware.terminate) { + httpServer.use(globalMiddleware.terminate, 'terminate') + } + }) + } +} diff --git a/src/Exceptions/AlreadyBootedException.ts b/src/Exceptions/AlreadyBootedException.ts new file mode 100644 index 0000000..aba0516 --- /dev/null +++ b/src/Exceptions/AlreadyBootedException.ts @@ -0,0 +1,23 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Exception } from '@secjs/utils' + +export class AlreadyBootedException extends Exception { + public constructor(resource: string) { + const content = `The resource "${resource}" is already booted.` + + super( + content, + 500, + 'ALREADY_BOOTED_ERROR', + `Try calling "shutdown${resource}" method to shutdown this resource`, + ) + } +} diff --git a/src/Exceptions/AlreadyShutdownException.ts b/src/Exceptions/AlreadyShutdownException.ts new file mode 100644 index 0000000..cb67232 --- /dev/null +++ b/src/Exceptions/AlreadyShutdownException.ts @@ -0,0 +1,23 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Exception } from '@secjs/utils' + +export class AlreadyShutdownException extends Exception { + public constructor(resource: string) { + const content = `The resource "${resource}" is already shutdown.` + + super( + content, + 500, + 'ALREADY_SHUTDOWN_ERROR', + `Try calling "boot${resource}" method to boot this resource`, + ) + } +} diff --git a/src/Exceptions/DuplicatedApplicationException.ts b/src/Exceptions/DuplicatedApplicationException.ts new file mode 100644 index 0000000..8d52db9 --- /dev/null +++ b/src/Exceptions/DuplicatedApplicationException.ts @@ -0,0 +1,24 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Exception } from '@secjs/utils' + +export class DuplicatedApplicationException extends Exception { + public constructor() { + const content = + 'An instance of application class has already been created inside this class.' + + super( + content, + 500, + 'DUPLICATED_APPLICATION_ERROR', + `Try calling "getApplication" method to get the instance of the application class.`, + ) + } +} diff --git a/src/Exceptions/NotBootedException.ts b/src/Exceptions/NotBootedException.ts new file mode 100644 index 0000000..0ed3404 --- /dev/null +++ b/src/Exceptions/NotBootedException.ts @@ -0,0 +1,23 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Exception } from '@secjs/utils' + +export class NotBootedException extends Exception { + public constructor(resource: string) { + const content = `The resource "${resource}" has not been booted.` + + super( + content, + 500, + 'NOT_BOOTED_ERROR', + `Try calling "boot${resource}" method to boot this resource`, + ) + } +} diff --git a/src/Exceptions/NullApplicationException.ts b/src/Exceptions/NullApplicationException.ts new file mode 100644 index 0000000..3b7c982 --- /dev/null +++ b/src/Exceptions/NullApplicationException.ts @@ -0,0 +1,23 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Exception } from '@secjs/utils' + +export class NullApplicationException extends Exception { + public constructor() { + const content = 'The application is null inside this class instance.' + + super( + content, + 500, + 'NULL_APPLICATION_ERROR', + `Try calling "createApplication" method to create the instance of the application class.`, + ) + } +} diff --git a/src/Factories/AthennaFactory.ts b/src/Factories/AthennaFactory.ts deleted file mode 100644 index b026e49..0000000 --- a/src/Factories/AthennaFactory.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @athenna/core - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Path } from '@secjs/utils' -import { Logger } from '@athenna/logger' -import { normalize, parse } from 'path' -import { Http, Router } from '@athenna/http' -import { resolveEnvFile } from '@athenna/config' -import { getAppEnvironment } from 'src/Utils/getAppEnvironment' -import { ResolveClassExport } from 'src/Utils/ResolveClassExport' -import { AthennaErrorHandler } from 'src/Utils/AthennaErrorHandler' - -export class AthennaFactory { - private static logger: any - private static extension: '.js' | '.ts' - - constructor(fileName: string) { - AthennaFactory.resolveNodeTs(fileName) - - process.env.NODE_ENV = getAppEnvironment( - Path.config(`app${AthennaFactory.extension}`), - ) - - resolveEnvFile() - Config.load(Path.config()) - - AthennaFactory.logger = new Logger().channel('application', { - formatterConfig: { - context: AthennaFactory.name, - }, - }) - - if ( - !Env('APP_DEBUG') && - (Env('NODE_ENV') === 'test' || Env('NODE_ENV') === 'testing') - ) { - AthennaFactory.logger = { - log: () => {}, - error: () => {}, - } - } else { - console.clear() - } - - const providers = AthennaFactory.getProviders() - - AthennaFactory.registerProviders(providers) - AthennaFactory.bootProviders(providers) - AthennaFactory.preloadFiles() - } - - private static getProviders() { - const providers = Config.get('app.providers') - const providersNormalized: any[] = [] - - providers.forEach(Provider => { - providersNormalized.push(ResolveClassExport.resolve(Provider)) - }) - - providersNormalized.forEach(Provider => - AthennaFactory.logger.log(`Registering ${Provider.name}`), - ) - - return providersNormalized - } - - private static bootProviders(providers: any[]) { - providers.forEach(Provider => { - const provider = new Provider() - - provider.registerAttributes() - - if (provider.boot) provider.boot() - }) - } - - private static registerProviders(providers: any[]) { - providers.forEach(Provider => { - const provider = new Provider() - - provider.registerAttributes() - - if (provider.register) provider.register() - }) - } - - private static preloadFiles() { - const preloads = Config.get('app.preloads') - - preloads.forEach(preload => { - preload = normalize(preload) - - const { dir, name } = parse(Path.config(preload)) - AthennaFactory.logger.log(`Preloading ${name} file`) - - require(`${dir}/${name}${this.extension}`) - }) - } - - private static resolveNodeTs(fileName: string) { - const { ext } = parse(fileName) - - if (ext === '.ts') process.env.NODE_TS = 'true' - else process.env.NODE_TS = 'false' - - AthennaFactory.extension = ext as any - } - - async http(): Promise { - const http = ioc.safeUse('Athenna/Core/HttpServer') - const route = ioc.safeUse('Athenna/Core/HttpRoute') - - http.setErrorHandler(AthennaErrorHandler.http) - - route.register() - - const port = Config.get('app.port') - const host = Config.get('app.host') - - await http.listen(port, host) - - AthennaFactory.logger.log(`Http server started on http://${host}:${port}`) - - return http - } -} diff --git a/src/Ignite.ts b/src/Ignite.ts index 399fc20..bca6841 100644 --- a/src/Ignite.ts +++ b/src/Ignite.ts @@ -13,22 +13,243 @@ import '@athenna/ioc' import '@athenna/config/src/Utils/global' -import { Http } from '@athenna/http' -import { AthennaFactory } from 'src/Factories/AthennaFactory' +import { Logger } from '@athenna/logger' +import { File, Path } from '@secjs/utils' +import { Application } from 'src/Application' +import { resolveEnvFile } from '@athenna/config' +import { normalize, parse, resolve } from 'path' +import { ResolveClassExport } from 'src/Utils/ResolveClassExport' +import { DuplicatedApplicationException } from 'src/Exceptions/DuplicatedApplicationException' export class Ignite { - private athennaFactory: AthennaFactory + /** + * Simple logger for Ignite class. + * + * @private + */ + private logger: Logger + + /** + * An instance of the application. Is here that the + * client will bootstrap his type of application. + * + * @private + */ + private application: Application | null + + /** + * The extension that is being used to bootstrap the + * application. This will be usefully to preload files. + * + * @private + */ + private extension: '.ts' | '.js' public constructor(fileName: string) { - this.athennaFactory = new AthennaFactory(fileName) + /** + * Change all process.cwd commands to return the + * root path where the application root is stored. + * + * It keeps like that: + * go out from athenna core src folder -> + * go out from athenna core root folder -> + * go out from athenna packages folder -> + * go out from node_modules folder. + * + * Now process.chdir is in the application root. + */ + if (!process.env.CORE_TESTING) { + process.chdir(resolve(__dirname, '..', '..', '..', '..')) + } + + this.resolveNodeTs(fileName) + this.resolveNodeEnv() + + /** + * Load all config files of config folder + */ + Config.load(Path.config()) + + this.clearConsole() + + /** + * Using require because logger needs to be set after + * resolveNodeEnv method has been called. + */ + this.logger = ResolveClassExport.resolve(require('./Utils/Logger')) + + const providers = this.getProviders() + + this.registerProviders(providers) + this.bootProviders(providers) + this.preloadFiles() + } + + /** + * Get the instance of the application inside of Ignite class. + * + * @return Application + */ + getApplication(): Application { + if (!this.application) { + throw new DuplicatedApplicationException() + } + + return this.application } - // TODO - // worker() {} - // TODO - // command() {} + /** + * Create a new instance of application inside + * Ignite class. + * + * @return Application + */ + createApplication(): Application { + if (this.application) { + throw new DuplicatedApplicationException() + } + + this.application = new Application(this.extension) + + return this.application + } + + /** + * Resolve the NODE_TS Env variable verifying if the extension + * of the file that is running Ignite class is .ts or other. + * + * @param fileName + * @private + */ + private resolveNodeTs(fileName: string) { + const { ext } = parse(fileName) + + if (ext === '.ts') { + process.env.NODE_TS = 'true' + + this.extension = '.ts' + + return + } + + process.env.NODE_TS = 'false' + this.extension = (ext || '') as any + } + + /** + * Resolve the NODE_ENV Env variable verifying if it's already + * set. If not, get the content of config/app file and take the + * environment key value to set as NODE_ENV. Then, resolve the + * .env.${NODE_ENV} file in Node.js process. + * + * @private + */ + private resolveNodeEnv() { + if (!process.env.NODE_ENV) { + let env = new File(Path.config(`app${this.extension}`)) + .loadSync() + .getContentSync() + .toString() + .split('environment:')[1] + .split(',')[0] + .trim() + + if (env.includes('process.env.NODE_ENV')) { + env = env.split('process.env.NODE_ENV')[1].replace('/||/g', '').trim() + } + + process.env.NODE_ENV = env + } + + resolveEnvFile() + } + + /** + * Clear the console if isn't in debug mode and NODE_ENV is + * not set to testing environment. + * + * @private + */ + private clearConsole() { + const isNotDebugModeOrTesting = + !Env('APP_DEBUG') && + (Env('NODE_ENV') === 'test' || Env('NODE_ENV') === 'testing') + + if (isNotDebugModeOrTesting) { + return + } + + console.clear() + } + + /** + * Get all the providers from config/app file with export normalized. + * + * export default, export, module.exports, etc. + * + * @private + */ + private getProviders() { + const providers = Config.get('app.providers') + const providersNormalized: any[] = [] + + providers.forEach(Provider => { + providersNormalized.push(ResolveClassExport.resolve(Provider)) + }) + + providersNormalized.forEach(p => this.logger.log(`Registering ${p.name}`)) + + return providersNormalized + } + + /** + * Boot all the providers calling the boot method + * and reading the register attributes inside providers. + * + * @private + */ + private bootProviders(providers: any[]) { + providers.forEach(Provider => { + const provider = new Provider() + + provider.registerAttributes() + + if (provider.boot) provider.boot() + }) + } + + /** + * Register all the providers calling the register method + * and reading the register attributes inside providers. + * + * @private + */ + private registerProviders(providers: any[]) { + providers.forEach(Provider => { + const provider = new Provider() + + provider.registerAttributes() + + if (provider.register) provider.register() + }) + } + + /** + * Preload all the files configured inside config/app + * file. + * + * @private + */ + private preloadFiles() { + const preloads = Config.get('app.preloads') + + preloads.forEach(preload => { + preload = normalize(preload) + + const { dir, name } = parse(Path.config(preload)) + this.logger.log(`Preloading ${name} file`) - async httpServer(): Promise { - return this.athennaFactory.http() + require(`${dir}/${name}${this.extension}`) + }) } } diff --git a/src/Utils/AthennaErrorHandler.ts b/src/Utils/AthennaErrorHandler.ts index 387e67d..2d5a7e9 100644 --- a/src/Utils/AthennaErrorHandler.ts +++ b/src/Utils/AthennaErrorHandler.ts @@ -16,7 +16,7 @@ export class AthennaErrorHandler { // static worker() {} // TODO - // static command() {} + // static console() {} static http({ error, request, response }: ErrorContextContract) { const code = error.code || error.name diff --git a/src/Utils/Logger.ts b/src/Utils/Logger.ts new file mode 100644 index 0000000..3254cbf --- /dev/null +++ b/src/Utils/Logger.ts @@ -0,0 +1,26 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Logger as AthennaLogger } from '@athenna/logger' + +/** + * Mocking the Logger to not show Athenna logs in test environment + */ +export const Logger: AthennaLogger = + Env('NODE_ENV') === 'test' + ? ({ + channel: (_channel: string, _runtimeConfig?: any) => {}, + log: (_message: any, _options: any = {}) => {}, + info: (_message: any, _options: any = {}) => {}, + warn: (_message: any, _options: any = {}) => {}, + error: (_message: any, _options: any = {}) => {}, + debug: (_message: any, _options: any = {}) => {}, + success: (_message: any, _options: any = {}) => {}, + } as AthennaLogger) + : new AthennaLogger() diff --git a/src/Utils/getAppEnvironment.ts b/src/Utils/getAppEnvironment.ts deleted file mode 100644 index 20759f4..0000000 --- a/src/Utils/getAppEnvironment.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @athenna/core - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { File } from '@secjs/utils' - -export function getAppEnvironment(path: string): string { - let env = new File(path) - .loadSync() - .getContentSync() - .toString() - .split('environment:')[1] - .split(',')[0] - .trim() - - if (env.includes('process.env.NODE_ENV')) { - env = env.split('process.env.NODE_ENV')[1].replace('/||/g', '').trim() - } - - return process.env.NODE_ENV || env -} diff --git a/tests/Stubs/app/Http/Kernel.ts b/tests/Stubs/app/Http/Kernel.ts new file mode 100644 index 0000000..f8e8e84 --- /dev/null +++ b/tests/Stubs/app/Http/Kernel.ts @@ -0,0 +1,17 @@ +import { HttpKernel } from '@athenna/http' + +export class Kernel extends HttpKernel { + /** + * The application's global HTTP middlewares. + * + * This middlewares are run during every request to your http server. + */ + protected globalMiddlewares = [] + + /** + * The application's named HTTP middlewares. + * + * Here you define all your named middlewares to use inside routes/http file. + */ + protected namedMiddlewares = {} +} diff --git a/tests/Stubs/config/app.ts b/tests/Stubs/config/app.ts index 2a41d49..15702fd 100644 --- a/tests/Stubs/config/app.ts +++ b/tests/Stubs/config/app.ts @@ -7,8 +7,21 @@ export default { | Default environment of the application. | */ + environment: process.env.NODE_ENV || 'production', + /* + |-------------------------------------------------------------------------- + | Application debug + |-------------------------------------------------------------------------- + | + | Set if the application will start in debug mode or not. If in debug mode, + | the application will show sensitive logs and return sensitive data on errors. + | + */ + + debug: Env('APP_DEBUG', 'false'), + /* |-------------------------------------------------------------------------- | Application Name @@ -19,6 +32,7 @@ export default { | other location. | */ + name: Env('APP_NAME', 'Athenna') || require('../package.json').name, /* @@ -31,6 +45,7 @@ export default { | other location. | */ + version: require('../package.json').version, /* @@ -46,42 +61,6 @@ export default { description: Env('APP_DESCRIPTION', 'Athenna Framework'), - /* - |-------------------------------------------------------------------------- - | Application host - |-------------------------------------------------------------------------- - | - | This value is the HOST of your application and its used to access your - | application. - | - */ - - host: Env('HOST', '127.0.0.1'), - - /* - |-------------------------------------------------------------------------- - | Application port - |-------------------------------------------------------------------------- - | - | This value is the PORT of your application and its used to access your - | application. - | - */ - - port: Env('PORT', 1335), - - /* - |-------------------------------------------------------------------------- - | Application domain - |-------------------------------------------------------------------------- - | - | This value is the APP_DOMAIN of your application and its used to access your - | application. - | - */ - - domain: Env('APP_DOMAIN', 'http://localhost:1335'), - /* |-------------------------------------------------------------------------- | Application key @@ -103,6 +82,7 @@ export default { | sitory. | */ + source: Env('APP_SOURCE', 'https://github.com'), /* @@ -114,6 +94,7 @@ export default { | main documentation of the API. | */ + documentation: Env('APP_DOMAIN', 'http://localhost:1335'), /* @@ -126,6 +107,7 @@ export default { | based on HTTP headers/query string. | */ + locale: Env('APP_LOCALE', 'pt'), /* @@ -136,6 +118,7 @@ export default { | Default authorization strategy for the entire application. | */ + authorization: { defaultStrategy: 'jwt', jwt: { @@ -155,6 +138,7 @@ export default { | this array to grant expanded functionality to your applications. | */ + providers: [ require('@athenna/http/src/Providers/HttpServerProvider'), require('@athenna/http/src/Providers/HttpRouteProvider'), @@ -171,5 +155,6 @@ export default { | array. | */ - preloads: ['../routes/http'], + + preloads: [], } diff --git a/tests/Stubs/config/http.ts b/tests/Stubs/config/http.ts new file mode 100644 index 0000000..2d20a32 --- /dev/null +++ b/tests/Stubs/config/http.ts @@ -0,0 +1,37 @@ +export default { + /* + |-------------------------------------------------------------------------- + | Application host + |-------------------------------------------------------------------------- + | + | This value is the HOST of your application and its used to access your + | application. + | + */ + + host: Env('HOST', '127.0.0.1'), + + /* + |-------------------------------------------------------------------------- + | Application port + |-------------------------------------------------------------------------- + | + | This value is the PORT of your application and its used to access your + | application. + | + */ + + port: Env('PORT', 1335), + + /* + |-------------------------------------------------------------------------- + | Application domain + |-------------------------------------------------------------------------- + | + | This value is the APP_DOMAIN of your application and its used to access your + | application. + | + */ + + domain: Env('APP_DOMAIN', 'http://localhost:1335'), +} diff --git a/tests/Unit/IgniteTest.ts b/tests/Unit/IgniteTest.ts index 0ed8598..6ae0e04 100644 --- a/tests/Unit/IgniteTest.ts +++ b/tests/Unit/IgniteTest.ts @@ -15,16 +15,18 @@ describe('\n IgniteTest', () => { new File(Path.tests('Stubs/.env.test')).loadSync().copySync(Path.pwd('.env.test')) new Folder(Path.tests('Stubs/config')).loadSync().copySync(Path.pwd('config')) new Folder(Path.tests('Stubs/routes')).loadSync().copySync(Path.pwd('routes')) + new Folder(Path.tests('Stubs/app')).copySync(Path.pwd('app')) + new File(Path.tests('Stubs/app/Http/Kernel.ts')).copySync(Path.pwd('app/Http/Kernel.ts')) }) it('should be able to ignite an Athenna http project', async () => { - const app = await new Ignite(__filename).httpServer() + const app = await new Ignite(__filename).createApplication().bootHttpServer() expect(Env('APP_NAME')).toBe('Athenna') expect(Env('APP_DOMAIN')).toBe('http://localhost:1335') expect(Config.get('app.name')).toBe('Athenna') - expect(Config.get('app.domain')).toBe('http://localhost:1335') + expect(Config.get('http.domain')).toBe('http://localhost:1335') const { json, statusCode } = await app.request({ method: 'GET', @@ -38,6 +40,7 @@ describe('\n IgniteTest', () => { }) afterAll(() => { + new Folder(Path.pwd('app')).removeSync() new Folder(Path.pwd('config')).removeSync() new Folder(Path.pwd('routes')).removeSync() new File(Path.pwd('.env.test')).removeSync()