├── .babelrc ├── .gitignore ├── .nodemonignore ├── .stylelintrc ├── CHANGELOG.MD ├── README.md ├── __tests__ ├── helper.spec.ts └── server.spec.ts ├── configs ├── jest.json ├── pm2 │ ├── development.json │ └── release.json ├── tsconfig-client.json ├── tsconfig-server.json └── webpack │ ├── common.js │ ├── development.js │ └── release.js ├── gulpfile.js ├── nodemon.json ├── package.json ├── src ├── api │ ├── components │ │ ├── index.ts │ │ ├── product │ │ │ ├── controller.ts │ │ │ ├── end-point.ts │ │ │ ├── index.d.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── service.ts │ │ └── show │ │ │ ├── controller.ts │ │ │ ├── index.d.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── schema.ts │ │ │ └── service.ts │ ├── errors │ │ ├── i18n │ │ │ ├── EN.json │ │ │ └── ES.json │ │ └── index.ts │ ├── index.ts │ └── middlewares │ │ └── index.ts ├── core │ ├── architecture │ │ └── index.ts │ ├── helpers │ │ └── index.ts │ ├── logger │ │ └── index.ts │ └── translations │ │ └── index.ts ├── env │ ├── development.json │ ├── index.ts │ ├── release.json │ └── tests.json ├── index.d.ts ├── index.ts └── middlewares │ └── index.ts └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", {"modules": false}], 4 | "react" 5 | ], 6 | "plugins": [ 7 | "react-hot-loader/babel" 8 | ], 9 | "env": { 10 | "release": { 11 | "presets": ["minify"] 12 | }, 13 | "tests": { 14 | "presets": ["env", "react"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | 5 | # output 6 | dist/ 7 | .sass-cache 8 | .DS_Store 9 | 10 | # Runtime data 11 | *.pid 12 | *.seed 13 | 14 | # IDE 15 | .idea/ 16 | 17 | # Coverage directory 18 | __tests__/__coverage__ 19 | 20 | # Dependencies 21 | node_modules/ 22 | package-lock.json 23 | yarn.lock 24 | 25 | # cache directory 26 | .npm 27 | .tscache 28 | 29 | # tests 30 | test/ 31 | integration/ 32 | -------------------------------------------------------------------------------- /.nodemonignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | dist/** 3 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.0.0](https://github.com/devdennysegura/netflix-app/releases/tag/v1.0.0): -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netflix App 2 | 3 | ## Requirements 4 | 5 | * Mac OS X, Windows, or Linux 6 | * [Node.js](https://nodejs.org/) version 9.0+ 7 | * [MongoDB](https://docs.mongodb.com/v3.2/administration/install-community/) latest version 8 | 9 | ## Quick start 10 | 11 | ````bash 12 | git clone git@github.com:DennySegura/netflix-app.git 13 | cd netflix-app 14 | npm install 15 | ```` 16 | 17 | ## Deploy 18 | 19 | ### Run server 20 | 21 | #### [Release Mode] 22 | 23 | ````bash 24 | npm run pr # Ejecuta el servidor 25 | npm run start-pr # Ejecuta el servidor + Front 26 | npm run apm-pr # Ejecuta aplicación con PM2 27 | ```` 28 | 29 | #### [Development Mode] 30 | 31 | - Para ejecutar de forma independiente el servidor y la aplicación los siguientes comandos en terminales diferentes. 32 | 33 | ````bash 34 | npm run client-dev 35 | npm run dev 36 | ```` 37 | 38 | - Para ejecutar de forma agrupada. 39 | 40 | ````bash 41 | npm run start-dev 42 | ```` 43 | 44 | ## Tests 45 | 46 | ````bash 47 | npm run jest 48 | npm run jest-watch 49 | ```` 50 | 51 | ## Heath check 52 | 53 | Endpoint to health check **http://{HOST}/api/health** this return uptime 54 | 55 | ### Endpoints 56 | 57 | **/all** to fetch all tv shows and save in your mongoDB 58 | **/shows/:genre** get all tv shows from a genre 59 | 60 | ### Genres 61 | 62 | Drama, Action, Fantasy, Horror, Romance, Crime, Thriller, Mystery, 63 | Science-Fiction, Comedy, Family, Music, Adventure, Espionage, Supernatural 64 | 65 | ### TVMaze Api 66 | 67 | http://www.tvmaze.com/api 68 | 69 | 70 | Thanks 71 | ------ 72 | 73 | **Denny Segura** © 2018+. Released under the [MIT License].
74 | 75 | [MIT License]: http://mit-license.org/ -------------------------------------------------------------------------------- /__tests__/helper.spec.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_CONFIG_DIR = `src/env/`; 2 | import 'jest'; 3 | import { helper } from '../src/core/helpers/index'; 4 | 5 | const sorteableArray:Array = [1 , 5, 6, 3, 2, 10, 4]; 6 | const groupableArray:Array = [ 7 | {name: 'juan', lastname: 'anselmo'}, 8 | {name: 'denny', lastname: 'segura'}, 9 | {name: 'juan', lastname: 'bellini'} 10 | ] 11 | const groupedObject:any = { 12 | "juan": [{name: 'juan', lastname: 'anselmo'}, {name: 'juan', lastname: 'bellini'}], 13 | "denny": [{name: 'denny', lastname: 'segura'}] 14 | }; 15 | const groupedArray: Array = [ 16 | { key: 'juan', value: 2 }, 17 | { key: 'denny', value: 1 } 18 | ]; 19 | describe('Helpers test suite', () => { 20 | it('It should expect group object keys', () => { 21 | expect(helper.groupBy(groupableArray, (user:any)=> user.name)).toMatchObject(groupedObject); 22 | }); 23 | it('It should expect create array from object', () => { 24 | const arr = helper.objectToArray(groupedObject, (key: string) => ({ key, value: groupedObject[key].length })); 25 | expect(arr).toEqual(expect.arrayContaining(groupedArray)); 26 | }); 27 | it('It should expect getting descent sorted array', () => { 28 | expect(sorteableArray.sort(helper.sortAB)).toEqual([10, 6, 5, 4, 3, 2, 1]); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/server.spec.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_CONFIG_DIR = `src/env/`; 2 | import 'jest'; 3 | import * as request from 'supertest'; 4 | import * as config from 'config'; 5 | import { Server } from '../src'; 6 | 7 | const app = Server.bootstrap().middlwre; 8 | const configApp = config.get('server'); 9 | const baseUrl = `${(configApp as any).baseUrl}`; 10 | 11 | describe('App express test suite', () => { 12 | it('It should expecting 200 http status code value when asking base url plus health route', () => { 13 | return request(app) 14 | .get(`${baseUrl}/health`) 15 | .expect(200); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /configs/jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootDir": "..", 3 | "coverageDirectory": "/__tests__/__coverage__/", 4 | "transform": { 5 | "^.+\\.(ts|tsx)$": "/node_modules/ts-jest/preprocessor.js" 6 | }, 7 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$", 8 | "roots": ["/src/","/__tests__/"], 9 | "moduleFileExtensions": ["ts", "tsx", "js", "json"], 10 | "collectCoverage": true, 11 | "testEnvironment": "node", 12 | "transformIgnorePatterns": ["/node_modules/"], 13 | "moduleDirectories": ["node_modules"], 14 | "globals": { 15 | "ts-jest": { 16 | "__REACT_DEVTOOLS_GLOBAL_HOOK__": true, 17 | "tsConfigFile": "configs/tsconfig-client.json" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /configs/pm2/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "netflix-app", 5 | "script": "/dist/index.js", 6 | "instances": "4", 7 | "exec_mode" : "cluster", 8 | "max_memory_restart": "2G", 9 | "error_file": "development-err.log", 10 | "out_file": "development-output.log" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /configs/pm2/release.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "netflix-app", 5 | "script": "./dist/index.js", 6 | "instances": "4", 7 | "exec_mode": "cluster", 8 | "max_memory_restart": "2G", 9 | "error_file": "release-err.log", 10 | "out_file": "release-output.log" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /configs/tsconfig-client.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "jsx": "react", 6 | "lib": [ 7 | "dom", 8 | "es2015.core", 9 | "es5", 10 | "es2015", 11 | "esnext.asynciterable" 12 | ], 13 | "rootDirs": [ 14 | "../src", 15 | "application" 16 | ], 17 | "sourceMap": false, 18 | "outDir": "../dist", 19 | "removeComments": true, 20 | "strict": true, 21 | "noImplicitAny": true, 22 | "strictPropertyInitialization": false, 23 | "skipLibCheck": true, 24 | "moduleResolution": "node" 25 | }, 26 | "exclude": [ 27 | "../node_modules/", 28 | "../__tests__/*.spec.ts", 29 | "../node_modules/**/*.d.ts" 30 | ], 31 | "include": [ 32 | "../src/app/**/*.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /configs/tsconfig-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "jsx": "react", 6 | "lib": [ 7 | "dom", 8 | "es2015.core", 9 | "es5", 10 | "es2015", 11 | "esnext.asynciterable" 12 | ], 13 | "rootDirs": [ 14 | "../src", 15 | "application" 16 | ], 17 | "sourceMap": false, 18 | "outDir": "../dist", 19 | "removeComments": true, 20 | "strict": true, 21 | "noImplicitAny": true, 22 | "strictPropertyInitialization": false, 23 | "skipLibCheck": true, 24 | "moduleResolution": "node" 25 | }, 26 | "exclude": [ 27 | "../node_modules/", 28 | "../__tests__/", 29 | "../node_modules/**/*.d.ts" 30 | ], 31 | "include": [ 32 | "../src/**/*.ts", 33 | "../src/**/*.d.ts", 34 | "../src/**/*.tsx" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /configs/webpack/common.js: -------------------------------------------------------------------------------- 1 | const {resolve} = require('path'); 2 | const {CheckerPlugin} = require('awesome-typescript-loader'); 3 | const StyleLintPlugin = require('stylelint-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | resolve: { 8 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 9 | }, 10 | context: resolve(__dirname, '../../src/app'), 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | loader: 'source-map-loader', 16 | exclude: /node_modules/ 17 | }, 18 | { 19 | test: /\.tsx?$/, 20 | loader: 'awesome-typescript-loader', 21 | options: { 22 | configFileName: 'configs/tsconfig-client.json' 23 | } 24 | }, 25 | { 26 | test: /\.css$/, 27 | use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }, 'postcss-loader'], 28 | }, 29 | { 30 | test: /\.scss$/, 31 | loaders: [ 32 | 'style-loader', 33 | { loader: 'css-loader', options: { importLoaders: 1, minimize: true } }, 34 | 'sass-loader' 35 | ], 36 | }, 37 | { 38 | test: /\.(jpe?g|png|gif|svg)$/i, 39 | loaders: [ 40 | 'file-loader?hash=sha512&digest=hex&name=img/[hash].[ext]', 41 | 'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false', 42 | ], 43 | }, 44 | { 45 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 46 | use: [{ 47 | loader: 'file-loader', 48 | options: { 49 | name: '[name].[ext]', 50 | outputPath: 'fonts/' 51 | } 52 | }] 53 | } 54 | ], 55 | }, 56 | plugins: [ 57 | new CheckerPlugin(), 58 | new StyleLintPlugin(), 59 | new HtmlWebpackPlugin({ 60 | template: 'index.html' 61 | }), 62 | ], 63 | externals: { 64 | 'react': 'React', 65 | 'react-dom': 'ReactDOM' 66 | }, 67 | performance: { 68 | hints: false 69 | }, 70 | devServer: { 71 | port: 3000, 72 | contentBase: "./", 73 | historyApiFallback: true 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /configs/webpack/development.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const webpack = require('webpack'); 3 | const commonConfig = require('./common'); 4 | const {resolve} = require('path'); 5 | const uglifyJsPlugin = require('uglifyjs-webpack-plugin'); 6 | 7 | module.exports = merge(commonConfig, { 8 | mode: 'development', 9 | output: { 10 | library: 'UserList', 11 | libraryTarget: 'umd', 12 | libraryExport: 'default', 13 | path: resolve(__dirname, 'dist/app'), 14 | filename: 'index.js', 15 | publicPath: '/' 16 | }, 17 | entry: [ 18 | 'react-hot-loader/patch', 19 | 'webpack-dev-server/client?http://localhost:3000', 20 | 'webpack/hot/only-dev-server', 21 | './index.tsx' 22 | ], 23 | devServer: { 24 | historyApiFallback: true, 25 | hot: true 26 | }, 27 | devtool: 'cheap-module-eval-source-map', 28 | plugins: [ 29 | new uglifyJsPlugin({ 30 | sourceMap: true 31 | }), 32 | new webpack.HotModuleReplacementPlugin(), // enable HMR globally 33 | new webpack.NamedModulesPlugin(), // prints more readable module names in the browser console on HMR updates 34 | ], 35 | }); 36 | -------------------------------------------------------------------------------- /configs/webpack/release.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const {resolve} = require('path'); 3 | 4 | const commonConfig = require('./common'); 5 | 6 | module.exports = merge(commonConfig, { 7 | mode: 'production', 8 | entry: './index.tsx', 9 | output: { 10 | filename: 'js/bundle.[hash].min.js', 11 | path: resolve(__dirname, '../../dist/app'), 12 | publicPath: '/', 13 | }, 14 | devtool: 'source-map', 15 | plugins: [], 16 | }); 17 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | 3 | gulp.task('copy-to-dist', () => { 4 | gulp.src(['./src/**/*','!./src/**/*.ts', '!./src/**/components/**/*']) 5 | .pipe(gulp.dest('./dist/')); 6 | }); 7 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "signal": "SIGTERM", 3 | "ignore": ["**/*.spec.ts", ".git", "node_modules"], 4 | "watch": ["src"], 5 | "exec": "npm start", 6 | "ext": "ts,tsx,json", 7 | "restartable": "rs", 8 | "verbose": true, 9 | "events": { 10 | "restart": "pkill nodemon" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netflix-app", 3 | "version": "1.0.0", 4 | "description": "Prueba tecnica front para mercado libre argentina", 5 | "main": "./dist/index.js", 6 | "private": true, 7 | "scripts": { 8 | "lint:sass": "stylelint ./src/**/*.scss", 9 | "lint:ts": "tslint './src/**/*.ts*' --format stylish --force", 10 | "lint": "npm run lint:ts && npm run lint:sass", 11 | "test": "export NODE_ENV=tests && jest --coverage --config=configs/jest.json", 12 | "test-watch": "jest --watch", 13 | "cp-to-dist": "gulp copy-to-dist", 14 | "mkdir-dist": "rm -rf dist && mkdir dist", 15 | "tsc": "tsc -p ./configs/tsconfig-server.json", 16 | "nodemon": "nodemon --exec 'ts-node --project ./configs/tsconfig-server.json --cache-directory .tscache' ./src/index.ts", 17 | "build": "npm run mkdir-dist && npm run tsc && npm run cp-to-dist && rm -rf dist/app", 18 | "build-client": "webpack --config=configs/webpack/release.js", 19 | "dev": "export NODE_ENV=development && clear && npm run mkdir-dist && npm run cp-to-dist && npm run nodemon", 20 | "pr": "export NODE_ENV=release && clear && npm run mkdir-dist && npm run cp-to-dist && npm run nodemon", 21 | "start-pr": "export NODE_ENV=release && npm run build && npm run build-client && npm run start", 22 | "start-dev": "export NODE_ENV=development && npm run build && npm run build-client && npm run start", 23 | "start": "node ./dist/index.js", 24 | "client-start": "npm run client-dev", 25 | "client-dev": "webpack-dev-server --config=configs/webpack/development.js --content-base dist/", 26 | "apm-pr": "export NODE_ENV=release && npm run build && npm run build-client && pm2 start configs/pm2/release.json", 27 | "apm-dev": "export NODE_ENV=development && npm run build && npm run build-client && pm2 start configs/pm2/development.json" 28 | }, 29 | "dependencies": { 30 | "axios": "^0.18.0", 31 | "body-parser": "^1.18.2", 32 | "compression": "^1.7.2", 33 | "config": "^1.30.0", 34 | "cors": "^2.8.4", 35 | "express": "^4.16.3", 36 | "express-healthcheck": "^0.1.0", 37 | "history": "^4.7.2", 38 | "http-status-codes": "^1.3.0", 39 | "i18n": "^0.8.3", 40 | "i18next": "^11.3.3", 41 | "i18next-browser-languagedetector": "^2.2.0", 42 | "lodash": "^4.17.10", 43 | "mongoose": "^5.3.12", 44 | "mongoose-simple-random": "^0.4.1", 45 | "morgan": "^1.9.0", 46 | "pm2": "^2.10.4", 47 | "query-string": "^6.1.0", 48 | "react": "^16.4.1", 49 | "react-dom": "^16.4.1", 50 | "react-helmet": "^5.2.0", 51 | "react-i18next": "^7.7.0", 52 | "react-lottie": "^1.2.3", 53 | "react-redux": "^5.0.7", 54 | "react-router": "^4.2.0", 55 | "react-router-dom": "^4.2.2", 56 | "redux": "^4.0.0", 57 | "redux-devtools-extension": "^2.13.5", 58 | "redux-logger": "^3.0.6", 59 | "redux-promise-middleware": "^5.1.1", 60 | "redux-thunk": "^2.3.0", 61 | "winston": "^3.0.0" 62 | }, 63 | "devDependencies": { 64 | "@types/body-parser": "^1.17.0", 65 | "@types/compression": "0.0.35", 66 | "@types/config": "0.0.34", 67 | "@types/cors": "^2.8.3", 68 | "@types/errorhandler": "0.0.32", 69 | "@types/express": "^4.11.1", 70 | "@types/express-serve-static-core": "^4.11.1", 71 | "@types/i18next-browser-languagedetector": "^2.0.1", 72 | "@types/jest": "^22.2.3", 73 | "@types/lodash": "^4.14.107", 74 | "@types/method-override": "0.0.31", 75 | "@types/mongoose": "^5.3.1", 76 | "@types/morgan": "^1.7.35", 77 | "@types/node": "^9.6.18", 78 | "@types/query-string": "^6.1.0", 79 | "@types/react": "^16.4.6", 80 | "@types/react-dom": "^16.0.6", 81 | "@types/react-helmet": "^5.0.6", 82 | "@types/react-i18next": "^7.6.1", 83 | "@types/react-redux": "^6.0.3", 84 | "@types/react-router": "^4.0.27", 85 | "@types/react-router-dom": "^4.2.7", 86 | "@types/redux-logger": "^3.0.6", 87 | "@types/supertest": "^2.0.4", 88 | "@types/winston": "^2.3.9", 89 | "awesome-typescript-loader": "^5.2.0", 90 | "css-loader": "^0.28.11", 91 | "extract-text-webpack-plugin": "^3.0.2", 92 | "file-loader": "^1.1.11", 93 | "gulp": "^3.9.1", 94 | "html-webpack-plugin": "^3.2.0", 95 | "image-webpack-loader": "^4.3.1", 96 | "jest": "^22.4.2", 97 | "node-sass": "^4.9.0", 98 | "nodemon": "^1.17.3", 99 | "postcss-loader": "^2.1.5", 100 | "react-addons-test-utils": "^15.6.2", 101 | "react-hot-loader": "^4.3.3", 102 | "react-test-renderer": "^16.4.1", 103 | "sass-loader": "^7.0.3", 104 | "source-map-loader": "^0.2.3", 105 | "style-loader": "^0.21.0", 106 | "stylelint": "^9.3.0", 107 | "stylelint-config-standard": "^18.2.0", 108 | "stylelint-webpack-plugin": "^0.10.5", 109 | "supertest": "^3.0.0", 110 | "ts-jest": "^22.4.4", 111 | "ts-node": "^6.0.0", 112 | "tslint": "^5.9.1", 113 | "typescript": "^2.8.3", 114 | "uglifyjs-webpack-plugin": "^1.2.7", 115 | "webpack": "^4.14.0", 116 | "webpack-cli": "^3.0.8", 117 | "webpack-dev-server": "^3.1.4", 118 | "webpack-merge": "^4.1.3" 119 | }, 120 | "repository": { 121 | "type": "git", 122 | "url": "git+https://github.com/DennySegura/netflix-app.git" 123 | }, 124 | "keywords": [ 125 | "mercado-libre", 126 | "react", 127 | "nodejs", 128 | "server", 129 | "front-end", 130 | "back-end", 131 | "Argentina", 132 | "best-practices" 133 | ], 134 | "author": "Denny Segura ", 135 | "license": "ISC", 136 | "bugs": { 137 | "url": "https://github.com/DennySegura/netflix-app/issues" 138 | }, 139 | "homepage": "https://github.com/DennySegura/netflix-app#readme" 140 | } 141 | -------------------------------------------------------------------------------- /src/api/components/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Request, Response, NextFunction } from 'express'; 3 | import { productRoutes } from './product'; 4 | import { tvshowRoutes } from './show'; 5 | 6 | const apiRoutes: Scheme = { 7 | '/': { 8 | verb: ['get'], 9 | handler: (req: Request, res: Response, next: NextFunction) => { 10 | res.json({}); 11 | } 12 | } 13 | }; 14 | export const routesApi: Scheme = { 15 | ...apiRoutes, 16 | ...tvshowRoutes, 17 | ...productRoutes, 18 | }; 19 | -------------------------------------------------------------------------------- /src/api/components/product/controller.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Request, Response, NextFunction } from 'express'; 3 | import { Service } from './service'; 4 | 5 | export const Controller = { 6 | getItems: async (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | const result: any = await Service.getItems(req.params, {}); 9 | res.json(result); 10 | } 11 | catch (err) { 12 | next(err); 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/components/product/end-point.ts: -------------------------------------------------------------------------------- 1 | import * as config from 'config'; 2 | import * as _ from 'lodash'; 3 | const SERVER_LOGGER = 'server'; 4 | const { apiBasePath } = config.get(SERVER_LOGGER); 5 | 6 | export const endPoints = { 7 | 'tv-show': _.template(`${apiBasePath}/shows/<%= number %>?embed[]=episodes&embed[]=cast`) 8 | }; -------------------------------------------------------------------------------- /src/api/components/product/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Item { 2 | id: number; 3 | url: string; 4 | name: string; 5 | type: string; 6 | language: string; 7 | genres: string[]; 8 | status: string; 9 | runtime: number; 10 | premiered: string; 11 | officialSite: string; 12 | schedule: Schedule; 13 | rating: Rating; 14 | weight: number; 15 | network: Network; 16 | webChannel?: any; 17 | externals: Externals; 18 | image: Image; 19 | summary: string; 20 | updated: number; 21 | _links: Links; 22 | _embedded: Embedded; 23 | } 24 | 25 | interface Embedded { 26 | episodes: Episode[]; 27 | cast: Cast[]; 28 | } 29 | 30 | interface Cast { 31 | person: Person; 32 | character: Character; 33 | self: boolean; 34 | voice: boolean; 35 | } 36 | 37 | interface Character { 38 | id: number; 39 | url: string; 40 | name: string; 41 | image?: Image; 42 | _links: Links2; 43 | } 44 | 45 | interface Person { 46 | id: number; 47 | url: string; 48 | name: string; 49 | country?: Country; 50 | birthday?: string; 51 | deathday?: any; 52 | gender?: string; 53 | image: Image; 54 | _links: Links2; 55 | } 56 | 57 | interface Episode { 58 | id: number; 59 | url: string; 60 | name: string; 61 | season: number; 62 | number: number; 63 | airdate: string; 64 | airtime: string; 65 | airstamp: string; 66 | runtime: number; 67 | image: Image; 68 | summary: string; 69 | _links: Links2; 70 | } 71 | 72 | interface Links2 { 73 | self: Self; 74 | } 75 | 76 | interface Links { 77 | self: Self; 78 | previousepisode: Self; 79 | } 80 | 81 | interface Self { 82 | href: string; 83 | } 84 | 85 | interface Image { 86 | medium: string; 87 | original: string; 88 | } 89 | 90 | interface Externals { 91 | tvrage: number; 92 | thetvdb: number; 93 | imdb: string; 94 | } 95 | 96 | interface Network { 97 | id: number; 98 | name: string; 99 | country: Country; 100 | } 101 | 102 | interface Country { 103 | name: string; 104 | code: string; 105 | timezone: string; 106 | } 107 | 108 | interface Rating { 109 | average: number; 110 | } 111 | 112 | interface Schedule { 113 | time: string; 114 | days: string[]; 115 | } -------------------------------------------------------------------------------- /src/api/components/product/index.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from './controller'; 2 | export const productRoutes: { [index: string]: any } = { 3 | '/all': { 4 | verb: ['get'], 5 | handler: Controller.getItems 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/components/product/model.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export default class TvShow implements Item { 3 | 4 | public id: number; 5 | public url: string; 6 | public name: string; 7 | public type: string; 8 | public language: string; 9 | public genres: string[]; 10 | public status: string; 11 | public runtime: number; 12 | public premiered: string; 13 | public officialSite: string; 14 | public schedule: Schedule; 15 | public rating: Rating; 16 | public weight: number; 17 | public network: Network; 18 | public webChannel?: any; 19 | public externals: Externals; 20 | public image: Image; 21 | public summary: string; 22 | public updated: number; 23 | public _links: Links; 24 | public _embedded: Embedded; 25 | 26 | constructor(item: any) { 27 | this.item = item; 28 | } 29 | set item(item: any) { 30 | this.id = item.id; 31 | this.url = item.url; 32 | this.name = item.name; 33 | this.type = item.type; 34 | this.language = item.language; 35 | this.genres = item.genres; 36 | this.status = item.status; 37 | this.runtime = item.runtime; 38 | this.premiered = item.premiered; 39 | this.officialSite = item.officialSite; 40 | this.schedule = item.schedule; 41 | this.rating = item.rating; 42 | this.weight = item.weight; 43 | this.network = item.network; 44 | this.webChannel = item.webChannel; 45 | this.externals = item.externals; 46 | this.image = item.image; 47 | this.summary = item.summary; 48 | this.updated = item.updated; 49 | this._links = item._links; 50 | this._embedded = item._embedded; 51 | } 52 | get item(): any { 53 | const { id, name, image, genres, premiered, summary, _embedded, } = this; 54 | const data: any = { 55 | id, 56 | name, 57 | image: image ? image.original : null, 58 | details: { 59 | genres, 60 | year: premiered, 61 | description: summary, 62 | cast: _embedded ? _embedded.cast : null, 63 | episodes: _embedded ? _embedded.episodes : null 64 | } 65 | }; 66 | return data; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/api/components/product/service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import axios, { AxiosResponse } from 'axios'; 3 | import { endPoints } from './end-point'; 4 | import { helper as _ } from '../../../core/helpers'; 5 | import TvShow from './model'; 6 | import { addShow } from '../show/service'; 7 | const numTvShows = 50; 8 | 9 | export const Service = { 10 | getItems: async (body: any, headers: any) => { 11 | try { 12 | const options: any[] = Array.from({ length: numTvShows }).map((...params: any[]) => { 13 | const number: number = params[1] + 1; 14 | const url = endPoints['tv-show']({ number }); 15 | return axios.get(url).then(r => r).catch(e => null); 16 | }); 17 | const response: any = await Promise.all(options); 18 | const _response = response.map((el: any) => { 19 | if (el && el !== null && el.status === 200) { 20 | const tvShow: TvShow = new TvShow(el.data); 21 | return addShow(tvShow.item); 22 | } else { 23 | return null; 24 | } 25 | }); 26 | await Promise.all(_response); 27 | return { message: 'All items added to db' }; 28 | } 29 | catch (err) { 30 | throw { ...err, message: err.message, node_stack: new Error(err) }; 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/api/components/show/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { categories, byGenre, byId, shuffle } from './service'; 3 | 4 | export const Controller = { 5 | getItems: async (req: Request, res: Response, next: NextFunction) => { 6 | try { 7 | const result: any = await byGenre(req.params); 8 | res.json(result); 9 | } 10 | catch (err) { 11 | next(err); 12 | } 13 | }, 14 | getCategories: async (_: Request, res: Response, next: NextFunction) => { 15 | try { 16 | const result: any = await categories(); 17 | res.json(result); 18 | } 19 | catch (err) { 20 | next(err); 21 | } 22 | }, 23 | getRandom: async (_: Request, res: Response, next: NextFunction) => { 24 | try { 25 | const result: any = await shuffle(); 26 | res.json(result); 27 | } 28 | catch (err) { 29 | next(err); 30 | } 31 | }, 32 | getTvShow: async (req: Request, res: Response, next: NextFunction) => { 33 | try { 34 | const result: any = await byId(req.params); 35 | res.json(result); 36 | } 37 | catch (err) { 38 | next(err); 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/api/components/show/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/api/components/show/index.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from './controller'; 2 | 3 | export const tvshowRoutes: { [index: string]: any } = { 4 | '/home': { 5 | verb: ['get'], 6 | handler: Controller.getCategories 7 | }, 8 | '/shows/:genre': { 9 | verb: ['get'], 10 | handler: Controller.getItems 11 | }, 12 | '/show/:id': { 13 | verb: ['get'], 14 | handler: Controller.getTvShow 15 | }, 16 | '/shuffle-show': { 17 | verb: ['get'], 18 | handler: Controller.getRandom 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/api/components/show/model.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import { schema } from './schema'; 3 | interface IShowModel extends mongoose.Document { 4 | _id: String; 5 | id: Number; 6 | name: String; 7 | image: String; 8 | details: any; 9 | } 10 | export class ShowModel { 11 | 12 | private _showModel: IShowModel; 13 | 14 | constructor(_showModel: IShowModel) { 15 | this._showModel = _showModel; 16 | } 17 | 18 | get _id(): String { 19 | return this._showModel._id; 20 | } 21 | get id(): Number { 22 | return this._showModel.id; 23 | } 24 | get name(): String { 25 | return this._showModel.name; 26 | } 27 | get image(): String { 28 | return this._showModel.image; 29 | } 30 | get details(): any { 31 | return this._showModel.details; 32 | } 33 | } 34 | Object.seal(ShowModel); 35 | 36 | export const ShowSchemaModel: any = mongoose.model('show', schema, 'shows', true); -------------------------------------------------------------------------------- /src/api/components/show/schema.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import { Schema } from 'mongoose'; 3 | import * as random from 'mongoose-simple-random'; 4 | 5 | const ObjectId = mongoose.Types.ObjectId; 6 | ObjectId.prototype.valueOf = function () { 7 | return this.toString(); 8 | }; 9 | 10 | export const schema: any = new Schema( 11 | { 12 | id: Number, 13 | name: String, 14 | image: String, 15 | details: { 16 | genres: Array, 17 | year: String, 18 | description: String, 19 | cast: Array, 20 | episodes: Array 21 | } 22 | }, { 23 | timestamps: false, versionKey: false 24 | }); 25 | schema.plugin(random); -------------------------------------------------------------------------------- /src/api/components/show/service.ts: -------------------------------------------------------------------------------- 1 | import { ShowSchemaModel } from './model'; 2 | import { logger } from '../../../core/logger/index'; 3 | 4 | export const addShow = async ({ ...rest }: any) => { 5 | try { 6 | const show: any = await ShowSchemaModel.create({ ...rest }); 7 | return show; 8 | } catch (error) { 9 | logger.error(`Error on signup: ${error.message}`); 10 | throw error; 11 | } 12 | }; 13 | export const byGenre = async ({ genre }: any) => { 14 | try { 15 | const shows: any = await ShowSchemaModel.find({ 'details.genres': { $in: [genre] } }).limit(10); 16 | return shows; 17 | } catch (error) { 18 | logger.error(`Error on genre: ${error.message}`); 19 | throw error; 20 | } 21 | }; 22 | export const categories = async () => { 23 | const genres = ['Drama', 'Action', 'Fantasy', 'Horror', 'Romance', 'Crime', 'Thriller', 'Mystery', 24 | 'Science-Fiction', 'Comedy', 'Family', 'Music', 'Adventure', 'Espionage', 'Supernatural']; 25 | try { 26 | const shows: any = await Promise.all( 27 | genres.map((genre) => ShowSchemaModel.find({ 'details.genres': { $in: [genre] } }).limit(5)) 28 | ); 29 | return genres.map((genre, i) => ({ title: genre, items: shows[i] })); 30 | } catch (error) { 31 | logger.error(`Error on genre: ${error.message}`); 32 | throw error; 33 | } 34 | }; 35 | export const shuffle = async () => { 36 | try { 37 | const count: number = await ShowSchemaModel.count(); 38 | const random = Math.floor(Math.random() * count); 39 | const show: any = await ShowSchemaModel.findOne().skip(random); 40 | return show; 41 | } catch (error) { 42 | logger.error(`Error on random: ${error.message}`); 43 | throw error; 44 | } 45 | }; 46 | export const byId = async ({ id }: any) => { 47 | try { 48 | return await ShowSchemaModel.findOne({ id }); 49 | } catch (error) { 50 | logger.error(`Error on random: ${error.message}`); 51 | throw error; 52 | } 53 | }; -------------------------------------------------------------------------------- /src/api/errors/i18n/EN.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": { 3 | "0": { 4 | "title": "Oops, we had an error", 5 | "message": "Try again in a few minutes" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/api/errors/i18n/ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": { 3 | "0": { 4 | "title": "Ups, tuvimos un error", 5 | "message": "Volvé a intentar en unos minutos" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/api/errors/index.ts: -------------------------------------------------------------------------------- 1 | import { TranslationsService } from '../../core/translations'; 2 | 3 | export class ErrorService { 4 | private isTrustedError: boolean; 5 | private status: number; 6 | private code: string; 7 | private language: string; 8 | private message: string; 9 | private node_stack: any; 10 | 11 | constructor(value: any) { 12 | this.message = value.message; 13 | this.isTrustedError = value.isTrustedError || true; 14 | this.status = value.status || 500; 15 | this.code = value.code || '0'; 16 | this.language = value.language || 'ES'; 17 | this.node_stack = value.node_stack; 18 | } 19 | get Item() { 20 | const localesError = TranslationsService.getTranslationData(this.language, __dirname); 21 | return { 22 | response: localesError.errors[this.code], 23 | status: this.status, 24 | isTrustedError: this.isTrustedError, 25 | node_stack: { 26 | name: (this.node_stack || {}).name, 27 | message: (this.node_stack || {}).message, 28 | stack: (this.node_stack || {}).stack 29 | }, 30 | message: this.message 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Architecture } from '../core/architecture'; 2 | import { middlewaresApi } from './middlewares'; 3 | import { routesApi } from './components'; 4 | 5 | export class Api extends Architecture { 6 | constructor() { 7 | super(); 8 | this.mountRoutes(routesApi); 9 | this.useMiddlewares(middlewaresApi); 10 | } 11 | 12 | public static bootstrap(): any { 13 | return new Api().middlwre; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/api/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { ErrorRequestHandler, Request, Response, NextFunction } from 'express'; 3 | import * as httpStatus from 'http-status-codes'; 4 | import { ErrorService } from '../errors'; 5 | import { helper } from '../../core/helpers'; 6 | 7 | export const middlewaresApi: Scheme = { 8 | 'http-status-not-found': { 9 | mountPoint: '', 10 | handler: (error: ErrorRequestHandler, req: Request, res: Response, next: NextFunction) => { 11 | res.statusCode === httpStatus.NOT_FOUND && res.json({ message: httpStatus.NOT_FOUND }); 12 | next(error); 13 | } 14 | }, 15 | 'http-status-error': { 16 | mountPoint: '', 17 | handler: (error: any, req: Request, res: Response) => { 18 | const language = helper.headerLanguage(req.headers); 19 | const errorResponse: any = error ? new ErrorService({...error, language}).Item : httpStatus.INTERNAL_SERVER_ERROR; 20 | res.status(error.status ? error.status : httpStatus.INTERNAL_SERVER_ERROR).json({ error: errorResponse }); 21 | } 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/core/architecture/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as express from 'express'; 3 | import * as http from 'http'; 4 | import { RequestHandler } from 'express-serve-static-core'; 5 | import * as config from 'config'; 6 | import { logger } from './../logger'; 7 | import * as mongoose from 'mongoose'; 8 | 9 | const CONFIG_SERVER = 'server'; 10 | const settings = config.get(CONFIG_SERVER); 11 | 12 | class ExpressApp implements DynamicSignature { 13 | public middlwre: express.Application; 14 | constructor() { 15 | this.middlwre = express(); 16 | } 17 | } 18 | 19 | export abstract class Architecture { 20 | private static port = process.env.PORT || Architecture.normalizePort(`${(settings as any).port}`) || 9001; 21 | public middlwre: any; 22 | public timeout: number; 23 | constructor() { 24 | this.middlwre = new ExpressApp().middlwre; 25 | this.middlwre.disable('x-powered-by'); 26 | this.timeout = 5000; 27 | } 28 | public use(...arg: any[]) { 29 | this.middlwre.use(...arg); 30 | return this; 31 | } 32 | public set(setting: string, val: any) { 33 | this.middlwre.set(setting, val); 34 | return this; 35 | } 36 | public listen(handle: any, listeningListener: Function | undefined) { 37 | this.middlwre.listen(handle, listeningListener); 38 | } 39 | public get(pathRoute: string | RegExp | (string | RegExp)[], req: RequestHandler) { 40 | this.middlwre.get(pathRoute, req); 41 | } 42 | public addLocals(name: string, local: any) { 43 | this.middlwre.locals[`${name}`] = local; 44 | } 45 | public mountRoutes(routes: any) { 46 | Object.keys(routes).forEach((key) => { 47 | for (const verb of routes[key].verb) { 48 | this.middlwre[verb](key, routes[key].handler); 49 | } 50 | }); 51 | } 52 | public useMiddlewares(middlewares: any) { 53 | Object.keys(middlewares).forEach((key) => { 54 | this.middlwre.use(middlewares[key].mountPoint, middlewares[key].handler); 55 | }); 56 | } 57 | public addOptions(options: any) { 58 | Object.keys(options).forEach((key) => { 59 | this.middlwre.use(options[key].mountPoint, options[key].handler); 60 | }); 61 | } 62 | public setStatics(options: any) { 63 | Object.keys(options).forEach((key) => { 64 | this.middlwre.use(options[key].virtualPath, express.static(options[key].realPath)); 65 | }); 66 | } 67 | public setStatic(middlwrPath: string): void { 68 | this.middlwre.use(`${(settings as any).baseUrl}`, express.static(middlwrPath)); 69 | } 70 | public setViewsEngine(viewEngine: string, dirname: string): void { 71 | this.middlwre.set('view engine', viewEngine); 72 | this.middlwre.set('views', dirname); 73 | } 74 | public static normalizePort(val: number | string): number | string | boolean { 75 | const port: number = (typeof val === 'string') ? parseInt(val, 10) : val; 76 | if (isNaN(port)) return false; 77 | else if (port >= 0) return port; 78 | else return false; 79 | } 80 | public setServerTimeOut(time: number){ 81 | this.timeout = time; 82 | } 83 | public setDatabase() { 84 | const _dbUrl = (settings as any).dbUrl; 85 | const _dbDebugMode = (settings as any).debugMode; 86 | const useNewUrlParser = (settings as any).mongoClient; 87 | mongoose.Promise = global.Promise; 88 | mongoose.set('debug', _dbDebugMode); // debug mode on 89 | try { 90 | mongoose.connect(_dbUrl, { useNewUrlParser }); 91 | } catch (err) { 92 | mongoose.createConnection(_dbUrl, { useNewUrlParser }); 93 | } 94 | mongoose.connection 95 | .once('open', () => logger.info(`Listening mongoDB`)) 96 | .on('error', (e: any) => { 97 | logger.error(`mongoDB Error: ${e.message}`); 98 | throw e; 99 | }); 100 | } 101 | public onError(error: NodeJS.ErrnoException): void { 102 | const addr: any = (this.middlwre.settings['address'] || {}).addr; 103 | const port: number = (this.middlwre.settings['address'] || {}).port; 104 | if (error.syscall !== 'listen') throw error; 105 | const bind = (typeof port === 'string') ? 'Pipe ' + port : 'Port ' + port; 106 | switch (error.code) { 107 | case 'EACCES': 108 | logger.error(`${bind} requires elevated privileges`); 109 | process.exit(1); 110 | break; 111 | case 'EADDRINUSE': 112 | logger.error(`${bind} is already in use`); 113 | process.exit(1); 114 | break; 115 | default: 116 | throw error; 117 | } 118 | } 119 | private onListening() { 120 | const addr: any = this.middlwre.settings['address'].addr; 121 | const port: number = this.middlwre.settings['address'].port; 122 | const bind = (typeof addr === 'string') ? `pipe ${addr}` : `port ${port}`; 123 | } 124 | public startListen() { 125 | const httpService = http.createServer(this.middlwre); 126 | httpService.timeout = this.timeout; 127 | this.middlwre.set('port', Architecture.port); 128 | httpService.listen(Architecture.port); 129 | this.middlwre.set('address', httpService.address()); 130 | httpService.on('listening', this.onListening.bind(this)); 131 | httpService.on('error', this.onError.bind(this)); 132 | logger.info(`Listening on ${Architecture.port} in ${(settings as any).env} enviroment`); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/core/helpers/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export const helper: Helper = { 3 | groupBy(list: Array, keyGetter: any) { 4 | const map: any = {}; 5 | list.forEach((item) => { 6 | const key: any = keyGetter(item); 7 | const collection: any = map[key]; 8 | if (!collection) { 9 | map[key] = [item]; 10 | } else { 11 | collection.push(item); 12 | } 13 | }); 14 | return map; 15 | }, 16 | objectToArray(obj: any, operator: any) { 17 | return Object.keys(obj).map(operator); 18 | }, 19 | sortAB(a: SorteableValue | number, b: SorteableValue | number) { 20 | return (typeof b === 'number' ? b : b.value) - (typeof a === 'number' ? a : a.value); 21 | }, 22 | headerLanguage(headers: any){ 23 | return (headers['x-language'] || (headers['accept-language'] as any).split(',')[0]).split('-')[1]; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/core/logger/index.ts: -------------------------------------------------------------------------------- 1 | import { format, createLogger, transports } from 'winston'; 2 | import * as config from 'config'; 3 | import * as packageJson from '../../../package.json'; 4 | 5 | const SERVER_LOGGER = 'server'; 6 | const { combine, timestamp, label, printf, align, colorize } = format; 7 | const name = (packageJson as any).name; 8 | const { env } = config.get(SERVER_LOGGER); 9 | const loggerFormat: string = printf((info: any) => { 10 | return `[${info.level}] ${info.timestamp} [${name} ${env}-mode]: ${info.message}`; 11 | }); 12 | const winstonConfig = { 13 | format: combine( 14 | label({ label: 'right meow!' }), 15 | timestamp(), 16 | colorize(), 17 | loggerFormat 18 | ), 19 | level: 'info', 20 | transports: [ 21 | new transports.Console({ 22 | level: env === 'development' ? 'debug' : 'info', 23 | colorize: true 24 | }) 25 | ] 26 | }; 27 | export const logger = createLogger(winstonConfig); 28 | -------------------------------------------------------------------------------- /src/core/translations/index.ts: -------------------------------------------------------------------------------- 1 | import * as i18n from 'i18n'; 2 | 3 | export class TranslationsService { 4 | public static getTranslationData(locale: string = 'ES', path: string): any { 5 | i18n.configure({ 6 | locales: ['ES', 'EN'], 7 | defaultLocale: 'es', 8 | directory: `${path}/i18n`, 9 | objectNotation: true 10 | }); 11 | i18n.setLocale(locale); 12 | return i18n.getCatalog(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/env/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "env": "development", 4 | "port": 7000, 5 | "apiBasePath": "http://api.tvmaze.com", 6 | "dbUrl": "mongodb://127.0.0.1:27017/shows-development", 7 | "baseUrl": "/api", 8 | "appName": "[development] Netflix App Api", 9 | "mongoClient": true, 10 | "debugMode": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/env/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as release from './release.json'; 3 | import * as development from './development.json'; 4 | import * as test from './tests.json'; 5 | 6 | export const RELEASE: string = (release as any).server.env; 7 | export const DEVELOPMENT: string = (development as any).server.env; 8 | export const TEST: string = (test as any).server.env; 9 | export const AUTHOR: IAuthor = { 10 | name: 'Denny', 11 | lastname: 'Segura' 12 | }; 13 | -------------------------------------------------------------------------------- /src/env/release.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "env": "release", 4 | "port": 7000, 5 | "apiBasePath": "http://api.tvmaze.com", 6 | "dbUrl": "mongodb://127.0.0.1:27017/shows-development", 7 | "baseUrl": "/api", 8 | "appName": "[release] Netflix App Api", 9 | "mongoClient": true, 10 | "debugMode": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/env/tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "env": "testing", 4 | "port": 7000, 5 | "apiBasePath": "http://api.tvmaze.com", 6 | "dbUrl": "mongodb://127.0.0.1:27017/shows-development", 7 | "baseUrl": "/api", 8 | "appName": "[testing] Netflix App Api", 9 | "mongoClient": true, 10 | "debugMode": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any; 3 | export default value; 4 | } 5 | declare module 'json!*' { 6 | const value: any; 7 | export default value; 8 | } 9 | declare module 'express-healthcheck'; 10 | declare module 'compression'; 11 | declare module 'http-status'; 12 | declare module 'mongoose'; 13 | declare module 'mongoose-simple-random'; 14 | declare module 'i18n'; 15 | declare module 'winston'; 16 | 17 | interface SorteableValue { 18 | key: string; 19 | value: number; 20 | } 21 | interface Scheme { 22 | [index: string]: any; 23 | } 24 | interface IAuthor{ 25 | name: string; 26 | lastname: string; 27 | } 28 | interface Helper { 29 | groupBy(list: any[], keyGetter: any): any; 30 | objectToArray(obj: any, operator: any): any[]; 31 | sortAB(a: any, b: any): number; 32 | headerLanguage(headers: any): any; 33 | } 34 | declare class ErrorService { 35 | private isTrustedError: boolean; 36 | private status: number; 37 | private code: string; 38 | private language: string; 39 | private message: string; 40 | private node_stack: any; 41 | constructor(value: any); 42 | public Item(): any; 43 | } 44 | declare class TranslationsService { 45 | public static getTranslationData(locale: string, path: string): any; 46 | } 47 | interface DynamicSignature { 48 | [key: string]: any; 49 | } 50 | declare class ExpressApp implements DynamicSignature { 51 | public middlwre: express.Application; 52 | constructor(); 53 | } 54 | declare abstract class Architecture { 55 | private static port: any; 56 | public middlwre: any; 57 | public timeout: number; 58 | constructor(); 59 | public use(...arg: any[]): Architecture; 60 | public set(setting: string, val: any): Architecture; 61 | public listen(handle: any, listeningListener: Function | undefined): void; 62 | public get(pathRoute: string | RegExp | (string | RegExp)[], req: RequestHandler): void; 63 | public addLocals(name: string, local: any): void; 64 | public mountRoutes(routes: any): void; 65 | public useMiddlewares(middlewares: any): void; 66 | public addOptions(options: any): void; 67 | public setStatics(options: any): void; 68 | public setStatic(middlwrPath: string): void; 69 | public setViewsEngine(viewEngine: string, dirname: string): void; 70 | public setServerTimeOut(time: number): void; 71 | public onError(error: NodeJS.ErrnoException): void; 72 | public startListen(): void; 73 | public static normalizePort(val: number | string): number | string | boolean; 74 | private onListening(): void; 75 | } 76 | declare class Server extends Architecture { 77 | constructor(); 78 | public static bootstrap(): Server; 79 | } 80 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_CONFIG_DIR = `${__dirname}/env`; 2 | import * as cors from 'cors'; 3 | import { Architecture } from './core/architecture'; 4 | import { middlewaresServer } from './middlewares'; 5 | 6 | export class Server extends Architecture { 7 | constructor() { 8 | super(); 9 | this.addOptions([{ mountPoint: '*', handler: cors() }]); 10 | this.useMiddlewares(middlewaresServer); 11 | this.setDatabase(); 12 | } 13 | public static bootstrap() { 14 | return new Server(); 15 | } 16 | } 17 | Server.bootstrap().startListen(); 18 | -------------------------------------------------------------------------------- /src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { ErrorRequestHandler, Request, Response, NextFunction } from 'express'; 3 | import * as bodyParser from 'body-parser'; 4 | import * as expressHealthcheck from 'express-healthcheck'; 5 | import * as config from 'config'; 6 | import * as compress from 'compression'; 7 | import * as morgan from 'morgan'; 8 | import * as cors from 'cors'; 9 | import { logger } from './../core/logger'; 10 | import { Api } from './../api'; 11 | import { ErrorService } from './../api/errors'; 12 | import { helper } from '../core/helpers'; 13 | 14 | const CONFIG_SERVER = 'server'; 15 | const ConfigServer = config.get(CONFIG_SERVER); 16 | const baseUrl = `${(ConfigServer as any).baseUrl}`; 17 | 18 | export const middlewaresServer: Scheme = { 19 | 'health-check': { 20 | mountPoint: `${baseUrl}/health`, 21 | handler: expressHealthcheck() 22 | }, 23 | 'cross-domain': { 24 | mountPoint: '', 25 | handler: cors() 26 | }, 27 | 'url-encoded-parser': { 28 | mountPoint: '', 29 | handler: bodyParser.urlencoded({ extended: true }) 30 | }, 31 | 'json-parser': { 32 | mountPoint: '', 33 | handler: bodyParser.json() 34 | }, 35 | 'compression': { 36 | mountPoint: '', 37 | handler: compress() 38 | }, 39 | 'http-request-logger': { 40 | mountPoint: '', 41 | handler: (req: Request, res: Response, next: NextFunction) => { 42 | (ConfigServer as any).env === 'development' && morgan('dev'); 43 | next(); 44 | } 45 | }, 46 | 'api': { 47 | mountPoint: `${baseUrl}`, 48 | handler: Api.bootstrap() 49 | }, 50 | 'error': { 51 | mountPoint: '', 52 | handler: (error: ErrorRequestHandler, req: Request, res: Response, next: NextFunction) => { 53 | const language = helper.headerLanguage(req.headers); 54 | const err_response: any = new ErrorService({...error, language}).Item; 55 | res.status(err_response.status).json({ data: err_response.response }); 56 | logger.error(err_response); 57 | if (!err_response.isTrustedError) 58 | process.exit(1); 59 | next(); 60 | } 61 | } 62 | }; -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "no-reference": false, 11 | "eofline": false, 12 | "quotemark": [ 13 | true, 14 | "single" 15 | ], 16 | "indent": false, 17 | "ordered-imports": [ 18 | false 19 | ], 20 | "max-line-length": [ 21 | true, 22 | 150 23 | ], 24 | "member-ordering": [ 25 | false 26 | ], 27 | "curly": false, 28 | "interface-name": [ 29 | false 30 | ], 31 | "array-type": [ 32 | false 33 | ], 34 | "no-empty-interface": false, 35 | "no-empty": false, 36 | "arrow-parens": false, 37 | "object-literal-sort-keys": false, 38 | "no-string-literal": false, 39 | "no-unused-expression": false, 40 | "no-console": false, 41 | "trailing-comma": false, 42 | "max-classes-per-file": [ 43 | false 44 | ], 45 | "ban-types": false, 46 | "variable-name": [ 47 | false 48 | ], 49 | "one-line": [ 50 | false 51 | ], 52 | "one-variable-per-declaration": [ 53 | false 54 | ] 55 | }, 56 | "rulesDirectory": [] 57 | } --------------------------------------------------------------------------------