├── .circleci └── config.yml ├── .env.dev ├── .env.test ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── README.md ├── docker-compose.yml ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── app.ts ├── config │ ├── db.ts │ ├── inversify.ts │ └── types.ts ├── controller │ ├── RegistrableController.ts │ ├── userController.ts │ └── vehicleController.ts ├── entity │ ├── user.ts │ └── vehicle.ts ├── repository │ ├── repository.ts │ ├── userRepository.ts │ └── vehicleRepository.ts ├── server.ts ├── service │ ├── userService.ts │ └── vehicleService.ts └── utils │ ├── exceptions.ts │ ├── logger.ts │ ├── response.ts │ └── secrets.ts ├── test ├── service │ └── userService.test.ts └── utils │ ├── userTestBuilder.ts │ └── vehicleTestBuilder.ts ├── tsconfig.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/node:8.9.4 6 | working_directory: ~/service 7 | steps: 8 | - checkout 9 | - run: 10 | name: Current branch 11 | command: echo ${CIRCLE_BRANCH} 12 | - restore_cache: 13 | keys: 14 | - dependencies-cache-{{ checksum "package.json" }} 15 | - dependencies-cache 16 | - run: 17 | name: Install dependencies 18 | command: npm install 19 | - run: 20 | name: Run test 21 | command: npm test 22 | - save_cache: 23 | paths: 24 | - node_modules 25 | key: dependencies-cache-{{ checksum "package.json" }} 26 | build: 27 | docker: 28 | - image: circleci/node:8.9.4 29 | working_directory: ~/service 30 | steps: 31 | - checkout 32 | - restore_cache: 33 | keys: 34 | - dist-cache-{{ .Environment.CIRCLE_BRANCH }}-{{ .Environment.CIRCLE_SHA1 }} 35 | - dependencies-cache-{{ checksum "package.json" }} 36 | - dependencies-cache 37 | - run: 38 | name: Build files 39 | command: npm run build 40 | - save_cache: 41 | key: dist-cache-{{ .Environment.CIRCLE_BRANCH }}-{{ .Environment.CIRCLE_SHA1 }} 42 | paths: 43 | - dist 44 | deploy: 45 | docker: 46 | - image: circleci/node:8.9.4 47 | working_directory: ~/service 48 | steps: 49 | - checkout 50 | - add_ssh_keys 51 | - restore_cache: 52 | keys: 53 | - dist-cache-{{ .Environment.CIRCLE_BRANCH }}-{{ .Environment.CIRCLE_SHA1 }} 54 | - run: 55 | name: Known host 56 | command: ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts 57 | - run: 58 | name: Remove existing files 59 | command: ssh $SSH_USER@$SSH_HOST "mkdir -p ~/services; rm -rf ~/services/${SERVICE}" 60 | - run: 61 | name: Copy build 62 | command: scp -r ~/service "${SSH_USER}@${SSH_HOST}:~/services/${SERVICE}" 63 | - run: 64 | name: Exec deploy 65 | command: ssh $SSH_USER@$SSH_HOST "cd ~/services/${SERVICE}; docker stack deploy --compose-file docker-compose.yml ${SERVICE}" 66 | 67 | workflows: 68 | version: 2 69 | build_and_deploy: 70 | jobs: 71 | - test 72 | - build: 73 | requires: 74 | - test 75 | - deploy: 76 | requires: 77 | - build 78 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | PORT = 3500 2 | 3 | DB_NAME = vehicle 4 | DB_HOST = localhost 5 | DB_PORT = 3306 6 | DB_USER = root 7 | DB_PASSWORD = 8 | DB_LOGGING = true 9 | 10 | LOGGING_ERROR_PATH = ./logs/errorsDev.log 11 | LOGGING_EXCEPTION_PATH = ./logs/exceptionsDev.log 12 | LOGGING_LEVEL_CONSOLE = debug 13 | LOGGING_LEVEL_FILE = debug -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | PORT = 3500 2 | 3 | DB_NAME = vehicle 4 | DB_HOST = localhost 5 | DB_PORT = 3306 6 | DB_USER = root 7 | DB_PASSWORD = 8 | DB_LOGGING = false 9 | 10 | LOGGING_ERROR_PATH = ./logs/errorsDev.log 11 | LOGGING_EXCEPTION_PATH = ./logs/exceptionsDev.log 12 | LOGGING_LEVEL_CONSOLE = debug 13 | LOGGING_LEVEL_FILE = debug -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Build 17 | public/css/main.css 18 | 19 | # Coverage reports 20 | coverage 21 | 22 | # API keys and secrets 23 | .env 24 | 25 | # Dependency directory 26 | node_modules 27 | bower_components 28 | 29 | # Editors 30 | .idea 31 | *.iml 32 | 33 | # OS metadata 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # Ignore built ts files 38 | dist/**/* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "request": "attach", 6 | "name": "Attach by Process ID", 7 | "processId": "${command:PickProcess}", 8 | "protocol": "inspector" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:carbon 2 | COPY . . 3 | RUN npm install 4 | RUN npm run build 5 | CMD [ "node", "dist/server.js" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodejs-starter 2 | Nodejs project starter using Onion Architecture 3 | 4 | ## Getting Started 5 | Async and non-blocking Nodejs 😎 6 | 7 | Fully based on onion architecture and good practices. 8 | 9 | This project is using: 10 | - TypeORM for connection to MySQL database 11 | - Inversifyjs as IoC (Inversion of Control) 12 | - Express as API handler 13 | - Docker for deploy as container 14 | - CircleCI for automating pipeline from commit to deploy 15 | 16 | ### Prerequisites 17 | You will need a MySQL database at port 3307 or actually you can change this port at: 18 | ``` 19 | .env - production (need to be created and set environment NODE_ENV=production) 20 | .env.dev - development 21 | .env.test - testing 22 | ``` 23 | 24 | ### Installing 25 | You should install dependencies to use this project 26 | ``` 27 | npm install 28 | ``` 29 | 30 | ### Running test 31 | Test can simply run with: 32 | ``` 33 | npm test 34 | ``` 35 | This test will show you a map coverage with Jest 36 | Simply test are localed at test/ 37 | For test you need to name your files as: **.test.ts 38 | 39 | ### Running develop 40 | You can watch file changes using nodemon simply running: 41 | ``` 42 | npm run watch 43 | ``` 44 | 45 | ### Deployment 46 | Using docker: 47 | ``` 48 | - docker build -t started:1.0.0 . 49 | - docker run --name started -p '3500:3500' -d started:1.0.0 50 | ``` 51 | 52 | Using Docker-compose: 53 | ``` 54 | docker-compose up -d 55 | ``` 56 | 57 | Using CircleCI: 58 | For circle workflows you should complete the last workflow step named as deploy (Everyone have a different deploy strategy) 59 | 60 | 61 | 62 | 63 | Regards 64 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | started: 4 | build: . 5 | ports: 6 | - 3500 -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | tsConfig: 'tsconfig.json' 5 | } 6 | }, 7 | moduleFileExtensions: [ 8 | 'ts', 9 | 'js' 10 | ], 11 | transform: { 12 | '^.+\\.(ts|tsx)$': 'ts-jest' 13 | }, 14 | testMatch: [ 15 | '**/test/**/*.test.(ts|js)' 16 | ], 17 | testEnvironment: 'node' 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-started", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "npm run serve", 6 | "build": "npm run build-ts && npm run tslint", 7 | "serve": "node dist/server.js", 8 | "watch-node": "nodemon dist/server.js", 9 | "watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"", 10 | "test": "jest --forceExit --coverage --verbose --detectOpenHandles", 11 | "watch-test": "npm run test -- --watchAll", 12 | "build-ts": "tsc", 13 | "watch-ts": "tsc -w", 14 | "tslint": "tslint -c tslint.json -p tsconfig.json", 15 | "debug": "npm run build && npm run watch-debug", 16 | "serve-debug": "nodemon --inspect dist/server.js", 17 | "watch-debug": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run serve-debug\"" 18 | }, 19 | "dependencies": { 20 | "body-parser": "^1.18.3", 21 | "compression": "^1.7.3", 22 | "cors": "^2.8.4", 23 | "dotenv": "^6.1.0", 24 | "errorhandler": "^1.5.0", 25 | "express": "^4.16.3", 26 | "helmet": "^3.13.0", 27 | "http-status": "^1.2.0", 28 | "inversify": "^4.13.0", 29 | "morgan": "^1.9.1", 30 | "mysql": "^2.16.0", 31 | "reflect-metadata": "^0.1.12", 32 | "typeorm": "^0.2.7", 33 | "winston": "^3.1.0" 34 | }, 35 | "devDependencies": { 36 | "@types/body-parser": "^1.17.0", 37 | "@types/chai": "^4.1.6", 38 | "@types/compression": "0.0.36", 39 | "@types/cors": "^2.8.4", 40 | "@types/dotenv": "^4.0.3", 41 | "@types/errorhandler": "0.0.32", 42 | "@types/express": "^4.16.0", 43 | "@types/helmet": "0.0.42", 44 | "@types/http-status": "^0.2.30", 45 | "@types/jest": "^23.3.5", 46 | "@types/morgan": "^1.7.35", 47 | "@types/node": "^10.11.6", 48 | "chai": "^4.2.0", 49 | "concurrently": "^4.0.1", 50 | "jest": "^23.6.0", 51 | "nodemon": "^1.18.4", 52 | "ts-jest": "^23.10.4", 53 | "ts-mockito": "^2.3.1", 54 | "ts-node": "^7.0.1", 55 | "tslint": "^5.11.0", 56 | "typescript": "^3.1.1" 57 | }, 58 | "author": "rankey1496", 59 | "license": "ISC" 60 | } 61 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import express, { Application, Request, Response, NextFunction } from 'express'; 3 | import compression from 'compression'; 4 | import helmet from 'helmet'; 5 | import errorHandler from 'errorhandler'; 6 | import morgan from 'morgan'; 7 | import cors from 'cors'; 8 | import bodyParser from 'body-parser'; 9 | import { container } from './config/inversify'; 10 | import { RegistrableController } from './controller/RegistrableController'; 11 | import Types from './config/types'; 12 | import { createConnection } from 'typeorm'; 13 | import { dbOptions } from './config/db'; 14 | import { logger } from './utils/logger'; 15 | import { NotFound, BadRequest, Unauthorize, Conflict } from './utils/exceptions'; 16 | import { notFoundResponse, badRequestResponse, unauthorizeResponse, conflictResponse, internalResponse } from './utils/response'; 17 | import { ENVIRONMENT } from './utils/secrets'; 18 | 19 | export default class App { 20 | 21 | private async init() { 22 | 23 | await createConnection(dbOptions); 24 | 25 | const app: Application = express(); 26 | app.set('port', process.env.PORT || 3000); 27 | 28 | app.use(errorHandler()); 29 | app.use(compression()); 30 | app.use(helmet()); 31 | app.use(morgan(ENVIRONMENT === 'production' ? 'combined' : 'dev')); 32 | app.use(cors()); 33 | app.use(bodyParser.json()); 34 | app.use(bodyParser.urlencoded({ extended: true })); 35 | 36 | const controllers: RegistrableController[] = container.getAll(Types.Controller); 37 | controllers.forEach(controller => controller.register(app)); 38 | 39 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => { 40 | logger.error(err.stack); 41 | if (err instanceof NotFound) { 42 | return notFoundResponse(res, err.message); 43 | } 44 | if (err instanceof BadRequest) { 45 | return badRequestResponse(res, err.message); 46 | } 47 | if (err instanceof Unauthorize) { 48 | return unauthorizeResponse(res, err.message); 49 | } 50 | if (err instanceof Conflict) { 51 | return conflictResponse(res, err.message); 52 | } 53 | return internalResponse(res); 54 | }); 55 | 56 | return Promise.resolve(app); 57 | } 58 | 59 | public async start() { 60 | const app = await this.init(); 61 | const server = app.listen(app.get('port'), async () => { 62 | console.log(`Service running at port ${app.get('port')} in ${app.get('env')} mode`); 63 | console.log('Date: ', new Date()); 64 | }); 65 | return Promise.resolve(server); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/config/db.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from 'typeorm'; 2 | import { DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_LOGGING } from '../utils/secrets'; 3 | import { Vehicle } from '../entity/vehicle'; 4 | import { User } from '../entity/user'; 5 | 6 | export const dbOptions: ConnectionOptions = { 7 | type: 'mysql', 8 | host: DB_HOST, 9 | port: DB_PORT, 10 | username: DB_USER, 11 | password: DB_PASSWORD, 12 | database: DB_NAME, 13 | entities: [ 14 | User, 15 | Vehicle 16 | ], 17 | logging: DB_LOGGING, 18 | synchronize: false 19 | }; -------------------------------------------------------------------------------- /src/config/inversify.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'inversify'; 2 | import { RegistrableController } from '../controller/RegistrableController'; 3 | import Types from './types'; 4 | import { VehicleController } from '../controller/vehicleController'; 5 | import { VehicleRepository } from '../repository/vehicleRepository'; 6 | import { VehicleService, VehicleServiceImp } from '../service/vehicleService'; 7 | import { UserService, UserServiceImp } from '../service/userService'; 8 | import { UserRepository } from '../repository/userRepository'; 9 | import { UserController } from '../controller/userController'; 10 | 11 | const container: Container = new Container(); 12 | 13 | // Controllers 14 | container.bind(Types.Controller).to(VehicleController); 15 | container.bind(Types.Controller).to(UserController); 16 | 17 | // Services 18 | container.bind(Types.VehicleService).to(VehicleServiceImp).inSingletonScope(); 19 | container.bind(Types.UserService).to(UserServiceImp).inSingletonScope(); 20 | 21 | // Repositories 22 | container.bind(Types.VehicleRepository).to(VehicleRepository).inSingletonScope(); 23 | container.bind(Types.UserRepository).to(UserRepository).inSingletonScope(); 24 | 25 | // Services 26 | 27 | export { container }; -------------------------------------------------------------------------------- /src/config/types.ts: -------------------------------------------------------------------------------- 1 | const Types = { 2 | 3 | Controller: Symbol('Controller'), 4 | 5 | VehicleRepository: Symbol('VehicleRepository'), 6 | UserRepository: Symbol('UserRepository'), 7 | 8 | VehicleService: Symbol('VehicleService'), 9 | UserService: Symbol('UserService') 10 | 11 | }; 12 | 13 | export default Types; -------------------------------------------------------------------------------- /src/controller/RegistrableController.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'express'; 2 | 3 | export interface RegistrableController { 4 | register(app: Application): void; 5 | } -------------------------------------------------------------------------------- /src/controller/userController.ts: -------------------------------------------------------------------------------- 1 | import { RegistrableController } from './RegistrableController'; 2 | import { Application, Request, NextFunction, Response } from 'express'; 3 | import { injectable, inject } from 'inversify'; 4 | import { dataResponse } from '../utils/response'; 5 | import Types from '../config/types'; 6 | import { UserService } from '../service/userService'; 7 | 8 | @injectable() 9 | export class UserController implements RegistrableController { 10 | 11 | @inject(Types.UserService) 12 | private userService: UserService; 13 | 14 | public register(app: Application): void { 15 | app.route('/user/all') 16 | .get(async (req: Request, res: Response, next: NextFunction) => { 17 | try { 18 | const result = await this.userService.getAll(); 19 | return dataResponse(res, result); 20 | } catch (error) { 21 | return next(error); 22 | } 23 | }); 24 | 25 | app.route('/user/byId/:id') 26 | .get(async (req: Request, res: Response, next: NextFunction) => { 27 | try { 28 | const id = req.params.id; 29 | const result = await this.userService.getById(id); 30 | return dataResponse(res, result); 31 | } catch (error) { 32 | return next(error); 33 | } 34 | }); 35 | 36 | app.route('/user/create/:name') 37 | .get(async (req: Request, res: Response, next: NextFunction) => { 38 | try { 39 | const name = req.params.name; 40 | const result = await this.userService.save(name); 41 | return dataResponse(res, result); 42 | } catch (error) { 43 | return next(error); 44 | } 45 | }); 46 | 47 | app.route('/user/:userId/vehicle/:vehicleId') 48 | .get(async (req: Request, res: Response, next: NextFunction) => { 49 | try { 50 | const userId = req.params.userId; 51 | const vehicleId = req.params.vehicleId; 52 | const result = await this.userService.newVehicle(userId, vehicleId); 53 | return dataResponse(res, result); 54 | } catch (error) { 55 | return next(error); 56 | } 57 | }); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/controller/vehicleController.ts: -------------------------------------------------------------------------------- 1 | import { RegistrableController } from './RegistrableController'; 2 | import { Application, Request, NextFunction, Response } from 'express'; 3 | import { injectable, inject } from 'inversify'; 4 | import { dataResponse } from '../utils/response'; 5 | import Types from '../config/types'; 6 | import { VehicleService } from '../service/vehicleService'; 7 | 8 | @injectable() 9 | export class VehicleController implements RegistrableController { 10 | 11 | @inject(Types.VehicleService) 12 | private vehicleService: VehicleService; 13 | 14 | public register(app: Application): void { 15 | app.route('/all') 16 | .get(async (req: Request, res: Response, next: NextFunction) => { 17 | try { 18 | const result = await this.vehicleService.getAll(); 19 | return dataResponse(res, result); 20 | // return dataResponse(res, 'Yololu'); 21 | } catch (error) { 22 | return next(error); 23 | } 24 | }); 25 | 26 | app.route('/byId/:id') 27 | .get(async (req: Request, res: Response, next: NextFunction) => { 28 | try { 29 | const id = req.params.id; 30 | const result = await this.vehicleService.getById(id); 31 | console.log(result); 32 | return dataResponse(res, result); 33 | // return dataResponse(res, 'Yololu'); 34 | } catch (error) { 35 | return next(error); 36 | } 37 | }); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm'; 2 | import { Vehicle } from './vehicle'; 3 | 4 | @Entity('User') 5 | export class User { 6 | 7 | @PrimaryGeneratedColumn() 8 | public id: number; 9 | 10 | @Column() 11 | public name: string; 12 | 13 | @ManyToMany(type => Vehicle, { eager: true }) 14 | @JoinTable() 15 | public vehicles: Vehicle[]; 16 | 17 | } -------------------------------------------------------------------------------- /src/entity/vehicle.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | 3 | @Entity('Vehicle') 4 | export class Vehicle { 5 | 6 | @PrimaryGeneratedColumn() 7 | public id: number; 8 | 9 | @Column() 10 | public name: string; 11 | 12 | } -------------------------------------------------------------------------------- /src/repository/repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository as typeRepository } from 'typeorm'; 2 | import { unmanaged, injectable } from 'inversify'; 3 | 4 | export type Query = { 5 | [P in keyof T]?: T[P] | { $regex: RegExp }; 6 | }; 7 | 8 | export interface Repository { 9 | findAll(): Promise; 10 | findById(id: string): Promise; 11 | findManyById(ids: string[]): Promise; 12 | findByQuery(query?: Query): Promise; 13 | update(id: string, item: T): Promise; 14 | save(data: T): Promise; 15 | delete(id: string): Promise; 16 | } 17 | 18 | @injectable() 19 | export abstract class GenericRepositoryImp implements Repository { 20 | 21 | private readonly repository: typeRepository; 22 | 23 | public constructor(@unmanaged() repository: typeRepository) { 24 | this.repository = repository; 25 | } 26 | 27 | public async findAll(): Promise { 28 | return await this.repository.find(); 29 | } 30 | 31 | public async findById(id: string): Promise { 32 | return await this.repository.findOne(id); 33 | } 34 | 35 | public async findManyById(ids: string[]): Promise { 36 | return await this.repository.findByIds(ids); 37 | } 38 | 39 | public async findByQuery(query: Query): Promise { 40 | return await this.repository.find(query); 41 | } 42 | 43 | public async update(id: string, data: any): Promise { 44 | const result = await this.repository.update(id, data); 45 | return !!result; 46 | } 47 | 48 | public async delete(id: string): Promise { 49 | const result = await this.repository.delete(id); 50 | return !!result; 51 | } 52 | 53 | public async save(data: any): Promise { 54 | const result = await this.repository.save(data); 55 | return !!result; 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/repository/userRepository.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { GenericRepositoryImp } from './repository'; 3 | import { getRepository } from 'typeorm'; 4 | import { User } from '../entity/user'; 5 | 6 | @injectable() 7 | export class UserRepository extends GenericRepositoryImp { 8 | 9 | constructor() { 10 | super(getRepository(User)); 11 | } 12 | } -------------------------------------------------------------------------------- /src/repository/vehicleRepository.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { GenericRepositoryImp } from './repository'; 3 | import { Vehicle } from '../entity/vehicle'; 4 | import { getRepository } from 'typeorm'; 5 | 6 | @injectable() 7 | export class VehicleRepository extends GenericRepositoryImp { 8 | 9 | constructor() { 10 | super(getRepository(Vehicle)); 11 | } 12 | } -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import App from './app'; 3 | 4 | console.info(` 5 | 888 888 888 d8b 888 .d8888b. d8b 6 | 888 888 888 Y8P 888 d88P Y88b Y8P 7 | 888 888 888 888 Y88b. 8 | Y88b d88P .d88b. 88888b. 888 .d8888b 888 .d88b. "Y888b. .d88b. 888d888 888 888 888 .d8888b .d88b. 9 | Y88b d88P d8P Y8b 888 "88b 888 d88P" 888 d8P Y8b "Y88b. d8P Y8b 888P" 888 888 888 d88P" d8P Y8b 10 | Y88o88P 88888888 888 888 888 888 888 88888888 "888 88888888 888 Y88 88P 888 888 88888888 11 | Y888P Y8b. 888 888 888 Y88b. 888 Y8b. Y88b d88P Y8b. 888 Y8bd8P 888 Y88b. Y8b. 12 | Y8P "Y8888 888 888 888 "Y8888P 888 "Y8888 "Y8888P" "Y8888 888 Y88P 888 "Y8888P "Y8888 13 | `); 14 | 15 | process.on('uncaughtException', (err) => { 16 | console.error(` 17 | -------------------- 18 | Unhandled Exception: 19 | ${err.message} 20 | -------------------- 21 | `); 22 | }); 23 | 24 | process.on('unhandledRejection', (err) => { 25 | console.error(` 26 | -------------------- 27 | Unhandled Rejection: 28 | ${err.message} 29 | -------------------- 30 | `); 31 | }); 32 | 33 | const app: App = new App(); 34 | app.start(); 35 | module.exports = app; -------------------------------------------------------------------------------- /src/service/userService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import Types from '../config/types'; 3 | import { NotFound, Conflict } from '../utils/exceptions'; 4 | import { User } from '../entity/user'; 5 | import { UserRepository } from '../repository/userRepository'; 6 | import { VehicleRepository } from '../repository/vehicleRepository'; 7 | 8 | export interface UserService { 9 | getAll(): Promise; 10 | getById(id: string): Promise; 11 | save(name: string): Promise; 12 | newVehicle(userId: string, vehicleId: string): Promise; 13 | } 14 | 15 | @injectable() 16 | export class UserServiceImp implements UserService { 17 | 18 | constructor( 19 | @inject(Types.UserRepository) private userRepository: UserRepository, 20 | @inject(Types.VehicleRepository) private vehicleRepository: VehicleRepository 21 | ) {} 22 | 23 | public async getAll(): Promise { 24 | return await this.userRepository.findAll(); 25 | } 26 | 27 | public async getById(id: string): Promise { 28 | const vehicle = await this.userRepository.findById(id); 29 | if (vehicle !== undefined) return vehicle; 30 | throw new NotFound('cant find tu madre'); 31 | } 32 | 33 | public async save(name: string): Promise { 34 | const created = await this.userRepository.save({ name }); 35 | if (created) return 'User created successfully'; 36 | throw new Conflict('Cant create new user'); 37 | } 38 | 39 | public async newVehicle(userId: string, vehicleId: string): Promise { 40 | const user = await this.userRepository.findById(userId); 41 | if (user !== undefined) { 42 | const vehicle = await this.vehicleRepository.findById(vehicleId); 43 | if (vehicle !== undefined) { 44 | user.vehicles.push(vehicle); 45 | console.log(user); 46 | await this.userRepository.save(user); 47 | return 'Vehicle added successfully'; 48 | } else { 49 | throw new NotFound('Cant find vehicles with that id'); 50 | } 51 | } else { 52 | throw new NotFound('Cant find user with that id'); 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/service/vehicleService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'inversify'; 2 | import { Vehicle } from '../entity/vehicle'; 3 | import Types from '../config/types'; 4 | import { VehicleRepository } from '../repository/vehicleRepository'; 5 | import { NotFound } from '../utils/exceptions'; 6 | 7 | export interface VehicleService { 8 | getAll(): Promise; 9 | getById(id: string): Promise; 10 | } 11 | 12 | @injectable() 13 | export class VehicleServiceImp implements VehicleService { 14 | 15 | constructor(@inject(Types.VehicleRepository) private vehicleRepository: VehicleRepository) {} 16 | 17 | public async getAll(): Promise { 18 | return await this.vehicleRepository.findAll(); 19 | } 20 | 21 | public async getById(id: string): Promise { 22 | const vehicle = await this.vehicleRepository.findById(id); 23 | if (vehicle !== undefined) return vehicle; 24 | throw new NotFound('cant find tu madre'); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/utils/exceptions.ts: -------------------------------------------------------------------------------- 1 | export class BadRequest extends Error {} 2 | 3 | export class Conflict extends Error {} 4 | 5 | export class NotFound extends Error {} 6 | 7 | export class Unauthorize extends Error {} -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { LoggerOptions, createLogger, transports } from 'winston'; 2 | import { LOGGING_LEVEL_CONSOLE, LOGGING_ERROR_PATH, LOGGING_LEVEL_FILE, LOGGING_EXCEPTION_PATH } from './secrets'; 3 | 4 | export const logger = createLogger( { 5 | exitOnError: false, 6 | transports: [ 7 | new transports.Console({ 8 | level: LOGGING_LEVEL_CONSOLE 9 | }), 10 | new transports.File({ 11 | filename: LOGGING_ERROR_PATH, 12 | level: LOGGING_LEVEL_FILE, 13 | maxsize: 1024 * 1024 * 10 14 | }) 15 | ], exceptionHandlers: [ 16 | new transports.File({ 17 | filename: LOGGING_EXCEPTION_PATH, 18 | level: LOGGING_LEVEL_FILE, 19 | maxsize: 1024 * 1024 * 10 20 | }) 21 | ] 22 | }); -------------------------------------------------------------------------------- /src/utils/response.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import { Response } from 'express'; 3 | 4 | function data(code: number, success: boolean, message: string) { 5 | return { 6 | code, 7 | success, 8 | message 9 | }; 10 | } 11 | 12 | export function dataResponse(res: Response, data: any) { 13 | return res.status(httpStatus.OK).json({ code: httpStatus.OK, success: true, data }); 14 | } 15 | 16 | export function notFoundResponse(res: Response, message: string) { 17 | return res.status(httpStatus.NOT_FOUND).json(data(httpStatus.NOT_FOUND, false, message)); 18 | } 19 | 20 | export function badRequestResponse(res: Response, message: string) { 21 | return res.status(httpStatus.BAD_REQUEST).json(data(httpStatus.BAD_REQUEST, false, message)); 22 | } 23 | 24 | export function unauthorizeResponse(res: Response, message: string) { 25 | return res.status(httpStatus.UNAUTHORIZED).json(data(httpStatus.UNAUTHORIZED, false, message)); 26 | } 27 | 28 | export function conflictResponse(res: Response, message: string) { 29 | return res.status(httpStatus.CONFLICT).json(data(httpStatus.CONFLICT, false, message)); 30 | } 31 | 32 | export function internalResponse(res: Response) { 33 | return res.status(httpStatus.INTERNAL_SERVER_ERROR).json(data(httpStatus.INTERNAL_SERVER_ERROR, false, 'Internal server error, try again later')); 34 | } -------------------------------------------------------------------------------- /src/utils/secrets.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import fs from 'fs'; 3 | 4 | function getEnvironment(path: string, env: string) { 5 | if (fs.existsSync(path)) { 6 | console.log(`Using ${env} environment variables`); 7 | dotenv.config({ path }); 8 | } else { 9 | console.error(`Can't load ${env} ${path} variables`); 10 | process.exit(1); 11 | } 12 | } 13 | 14 | export const ENVIRONMENT = process.env.NODE_ENV; 15 | 16 | if (ENVIRONMENT === 'production') { 17 | getEnvironment('.env', ENVIRONMENT); 18 | } else { 19 | if (ENVIRONMENT === 'test') { 20 | getEnvironment('.env.test', ENVIRONMENT); 21 | } else { 22 | getEnvironment('.env.dev', 'development'); 23 | } 24 | } 25 | 26 | /** 27 | * Database connection 28 | */ 29 | export const DB_NAME = process.env.DB_NAME; 30 | export const DB_HOST = process.env.DB_HOST; 31 | export const DB_PORT = Number(process.env.DB_PORT); 32 | export const DB_USER = process.env.DB_USER; 33 | export const DB_PASSWORD = process.env.DB_PASSWORD; 34 | export const DB_LOGGING = process.env.DB_LOGGING === 'true'; 35 | 36 | /** 37 | * Winston logger 38 | */ 39 | export const LOGGING_ERROR_PATH = process.env.LOGGING_ERROR_PATH; 40 | export const LOGGING_EXCEPTION_PATH = process.env.LOGGING_EXCEPTION_PATH; 41 | export const LOGGING_LEVEL_CONSOLE = process.env.LOGGING_LEVEL_CONSOLE; 42 | export const LOGGING_LEVEL_FILE = process.env.LOGGING_LEVEL_FILE; -------------------------------------------------------------------------------- /test/service/userService.test.ts: -------------------------------------------------------------------------------- 1 | import { instance, mock, when, anything } from 'ts-mockito'; 2 | import { expect } from 'chai'; 3 | import { UserRepository } from '../../src/repository/userRepository'; 4 | import { VehicleRepository } from '../../src/repository/vehicleRepository'; 5 | import { UserService, UserServiceImp } from '../../src/service/userService'; 6 | import { User } from '../../src/entity/user'; 7 | 8 | describe('UserService', () => { 9 | let userRepository: UserRepository; 10 | let vehicleRepository: VehicleRepository; 11 | let userService: UserService; 12 | 13 | beforeAll(async done => { 14 | userRepository = mock(UserRepository); 15 | vehicleRepository = mock(VehicleRepository); 16 | userService = new UserServiceImp(userRepository, vehicleRepository); 17 | done(); 18 | }); 19 | 20 | describe('getAll', () => { 21 | it('should get all', async () => { 22 | const result = await userService.getAll(); 23 | expect(true).to.be.equal(true); 24 | }); 25 | }); 26 | }); -------------------------------------------------------------------------------- /test/utils/userTestBuilder.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../src/entity/user'; 2 | import { Vehicle } from '../../src/entity/vehicle'; 3 | import VehicleTestBuilder from './vehicleTestBuilder'; 4 | 5 | export default class UserTestBuilder { 6 | 7 | private user: User = new User(); 8 | 9 | public static newUser() { 10 | return new UserTestBuilder(); 11 | } 12 | 13 | public withId(id: number): UserTestBuilder { 14 | this.user.id = id; 15 | return this; 16 | } 17 | 18 | public withName(name: string): UserTestBuilder { 19 | this.user.name = name; 20 | return this; 21 | } 22 | 23 | public withVehicles(vehicles: Array): UserTestBuilder { 24 | this.user.vehicles = vehicles; 25 | return this; 26 | } 27 | 28 | public withDefaultValues(): UserTestBuilder { 29 | return this.withId(1).withName('Jimbo') 30 | .withVehicles(VehicleTestBuilder.createListOfDefaultVehicles(5)); 31 | } 32 | 33 | public build(): User { 34 | return this.user; 35 | } 36 | 37 | public static createListOfDefaultUsers(size: number) { 38 | const result = []; 39 | for (let i = 0; i < size; i++) { 40 | result.push(UserTestBuilder.newUser().withDefaultValues().build()); 41 | } 42 | return result; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /test/utils/vehicleTestBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Vehicle } from '../../src/entity/vehicle'; 2 | 3 | export default class VehicleTestBuilder { 4 | 5 | private vehicle: Vehicle = new Vehicle(); 6 | 7 | public static newVehicle() { 8 | return new VehicleTestBuilder(); 9 | } 10 | 11 | public withId(id: number): VehicleTestBuilder { 12 | this.vehicle.id = id; 13 | return this; 14 | } 15 | 16 | public withName(name: string): VehicleTestBuilder { 17 | this.vehicle.name = name; 18 | return this; 19 | } 20 | 21 | public withDefaultValues(): VehicleTestBuilder { 22 | return this.withId(1).withName('Jimbo'); 23 | } 24 | 25 | public build(): Vehicle { 26 | return this.vehicle; 27 | } 28 | 29 | public static createListOfDefaultVehicles(size: number) { 30 | const result = []; 31 | for (let i = 0; i < size; i++) { 32 | result.push(VehicleTestBuilder.newVehicle().withDefaultValues().build()); 33 | } 34 | return result; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": [ 13 | "node_modules/*", 14 | "src/types/*" 15 | ] 16 | }, 17 | "types": [ 18 | "reflect-metadata" 19 | ], 20 | "typeRoots": [ 21 | "node_modules/@types" 22 | ], 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true 25 | }, 26 | "include": [ 27 | "src/**/*", 28 | "node_modules/@types" 29 | ] 30 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "one-line": [ 13 | true, 14 | "check-open-brace", 15 | "check-whitespace" 16 | ], 17 | "no-var-keyword": true, 18 | "quotemark": [ 19 | true, 20 | "single", 21 | "avoid-escape" 22 | ], 23 | "semicolon": [ 24 | true, 25 | "always", 26 | "ignore-bound-class-methods" 27 | ], 28 | "whitespace": [ 29 | true, 30 | "check-branch", 31 | "check-decl", 32 | "check-operator", 33 | "check-module", 34 | "check-separator", 35 | "check-type" 36 | ], 37 | "typedef-whitespace": [ 38 | true, 39 | { 40 | "call-signature": "nospace", 41 | "index-signature": "nospace", 42 | "parameter": "nospace", 43 | "property-declaration": "nospace", 44 | "variable-declaration": "nospace" 45 | }, 46 | { 47 | "call-signature": "onespace", 48 | "index-signature": "onespace", 49 | "parameter": "onespace", 50 | "property-declaration": "onespace", 51 | "variable-declaration": "onespace" 52 | } 53 | ], 54 | "no-internal-module": true, 55 | "no-trailing-whitespace": true, 56 | "no-null-keyword": true, 57 | "prefer-const": true, 58 | "jsdoc-format": true 59 | } 60 | } --------------------------------------------------------------------------------