├── .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 | }
--------------------------------------------------------------------------------