├── .dockerignore ├── .editorconfig ├── .env.example ├── .env.test ├── .gitignore ├── .travis.yml ├── .vscode ├── cSpell.json ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── appveyor.yml ├── commands ├── banner.ts ├── seed.ts └── tsconfig.ts ├── icon.png ├── nodemon.json ├── package-scripts.js ├── package.json ├── src ├── api │ ├── Context.ts │ ├── controllers │ │ ├── PetController.ts │ │ ├── UserController.ts │ │ ├── requests │ │ │ └── .gitkeep │ │ └── responses │ │ │ └── .gitkeep │ ├── errors │ │ ├── PetNotFoundError.ts │ │ └── UserNotFoundError.ts │ ├── interceptors │ │ └── .gitkeep │ ├── middlewares │ │ ├── CompressionMiddleware.ts │ │ ├── ErrorHandlerMiddleware.ts │ │ ├── LogMiddleware.ts │ │ ├── SecurityHstsMiddleware.ts │ │ ├── SecurityMiddleware.ts │ │ └── SecurityNoCacheMiddleware.ts │ ├── models │ │ ├── Pet.ts │ │ └── User.ts │ ├── repositories │ │ ├── PetRepository.ts │ │ └── UserRepository.ts │ ├── resolvers │ │ ├── PetResolver.ts │ │ └── UserResolver.ts │ ├── services │ │ ├── PetService.ts │ │ └── UserService.ts │ ├── subscribers │ │ ├── UserEventSubscriber.ts │ │ └── events.ts │ ├── types │ │ ├── Pet.ts │ │ ├── User.ts │ │ └── input │ │ │ └── PetInput.ts │ └── validators │ │ └── .gitkeep ├── app.ts ├── auth │ ├── AuthService.ts │ ├── authorizationChecker.ts │ └── currentUserChecker.ts ├── database │ ├── factories │ │ ├── PetFactory.ts │ │ └── UserFactory.ts │ ├── migrations │ │ ├── 1511105183653-CreateUserTable.ts │ │ ├── 1512663524808-CreatePetTable.ts │ │ └── 1512663990063-AddUserRelationToPetTable.ts │ └── seeds │ │ ├── CreateBruce.ts │ │ ├── CreatePets.ts │ │ └── CreateUsers.ts ├── decorators │ ├── DLoader.ts │ ├── EventDispatcher.ts │ └── Logger.ts ├── env.ts ├── lib │ ├── banner.ts │ ├── env │ │ ├── index.ts │ │ └── utils.ts │ ├── graphql │ │ ├── graphql-error-handling.ts │ │ └── index.ts │ └── logger │ │ ├── Logger.ts │ │ ├── LoggerInterface.ts │ │ └── index.ts ├── loaders │ ├── eventDispatchLoader.ts │ ├── expressLoader.ts │ ├── graphqlLoader.ts │ ├── homeLoader.ts │ ├── iocLoader.ts │ ├── monitorLoader.ts │ ├── publicLoader.ts │ ├── swaggerLoader.ts │ ├── typeormLoader.ts │ └── winstonLoader.ts ├── public │ └── favicon.ico └── types │ └── json.d.ts ├── test ├── e2e │ ├── api │ │ ├── info.test.ts │ │ └── users.test.ts │ └── utils │ │ ├── bootstrap.ts │ │ ├── server.ts │ │ └── typeormLoader.ts ├── integration │ └── PetService.test.ts ├── preprocessor.js ├── unit │ ├── auth │ │ └── AuthService.test.ts │ ├── lib │ │ ├── EventDispatcherMock.ts │ │ ├── LogMock.ts │ │ ├── RepositoryMock.ts │ │ └── setup.ts │ ├── middlewares │ │ └── ErrorHandlerMiddleware.test.ts │ ├── services │ │ └── UserService.test.ts │ └── validations │ │ └── UserValidations.test.ts └── utils │ ├── database.ts │ └── logger.ts ├── tsconfig.json ├── tslint.json ├── w3tec-divider.png ├── w3tec-logo.png └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | test 4 | .editorconfig 5 | .env.example 6 | .gitignore 7 | .travis.yml 8 | appveyor.yml 9 | icon.png 10 | LICENSE 11 | README.md -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # @w3tec 2 | # http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | # Use 2 spaces since npm does not respect custom indentation settings 18 | [package.json] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 2 | # APPLICATION 3 | # 4 | APP_NAME=express-typescript-boilerplate 5 | APP_SCHEMA=http 6 | APP_HOST=localhost 7 | APP_PORT=3000 8 | APP_ROUTE_PREFIX=/api 9 | APP_BANNER=true 10 | 11 | # 12 | # LOGGING 13 | # 14 | LOG_LEVEL=debug 15 | LOG_OUTPUT=dev 16 | 17 | # 18 | # PostgreSQL DATABASE 19 | # 20 | TYPEORM_CONNECTION=postgres 21 | TYPEORM_HOST=localhost 22 | TYPEORM_PORT=5432 23 | TYPEORM_USERNAME=username 24 | TYPEORM_PASSWORD= 25 | TYPEORM_DATABASE=my_database 26 | TYPEORM_SYNCHRONIZE=false 27 | TYPEORM_LOGGING=error 28 | TYPEORM_LOGGER=advanced-console 29 | 30 | # 31 | # MySQL DATABASE 32 | # 33 | # TYPEORM_CONNECTION=mysql 34 | # TYPEORM_HOST=localhost 35 | # TYPEORM_PORT=3306 36 | # TYPEORM_USERNAME=root 37 | # TYPEORM_PASSWORD=root 38 | # TYPEORM_DATABASE=my_database 39 | # TYPEORM_SYNCHRONIZE=false 40 | # TYPEORM_LOGGING=error 41 | # TYPEORM_LOGGER=advanced-console 42 | 43 | # 44 | # PATH STRUCTRUE 45 | # 46 | TYPEORM_MIGRATIONS=src/database/migrations/**/*.ts 47 | TYPEORM_MIGRATIONS_DIR=src/database/migrations 48 | TYPEORM_ENTITIES=src/api/models/**/*.ts 49 | TYPEORM_ENTITIES_DIR=src/api/models 50 | CONTROLLERS=src/api/controllers/**/*Controller.ts 51 | MIDDLEWARES=src/api/middlewares/**/*Middleware.ts 52 | INTERCEPTORS=src/api/interceptors/**/*Interceptor.ts 53 | SUBSCRIBERS=src/api/subscribers/**/*Subscriber.ts 54 | RESOLVERS=src/api/resolvers/**/*Resolver.ts 55 | 56 | # 57 | # GraphQL 58 | # 59 | GRAPHQL_ENABLED=true 60 | GRAPHQL_ROUTE=/graphql 61 | GRAPHQL_EDITOR=true 62 | 63 | # 64 | # Swagger 65 | # 66 | SWAGGER_ENABLED=true 67 | SWAGGER_ROUTE=/swagger 68 | SWAGGER_USERNAME=admin 69 | SWAGGER_PASSWORD=1234 70 | 71 | # 72 | # Status Monitor 73 | # 74 | MONITOR_ENABLED=true 75 | MONITOR_ROUTE=/monitor 76 | MONITOR_USERNAME=admin 77 | MONITOR_PASSWORD=1234 78 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 2 | # APPLICATION 3 | # 4 | APP_NAME=express-typescript-boilerplate 5 | APP_SCHEMA=http 6 | APP_HOST=localhost 7 | APP_PORT=3000 8 | APP_ROUTE_PREFIX=/api 9 | APP_BANNER=false 10 | 11 | # 12 | # LOGGING 13 | # 14 | LOG_LEVEL=none 15 | LOG_OUTPUT=dev 16 | 17 | # 18 | # DATABASE 19 | # 20 | TYPEORM_CONNECTION=sqlite 21 | TYPEORM_DATABASE=./mydb.sql 22 | TYPEORM_LOGGING=error 23 | TYPEORM_LOGGER=advanced-console 24 | 25 | # 26 | # PATH STRUCTRUE 27 | # 28 | TYPEORM_MIGRATIONS=src/database/migrations/**/*.ts 29 | TYPEORM_MIGRATIONS_DIR=src/database/migrations 30 | TYPEORM_ENTITIES=src/api/models/**/*.ts 31 | TYPEORM_ENTITIES_DIR=src/api/models 32 | CONTROLLERS=src/api/controllers/**/*Controller.ts 33 | MIDDLEWARES=src/api/middlewares/**/*Middleware.ts 34 | INTERCEPTORS=src/api/interceptors/**/*Interceptor.ts 35 | SUBSCRIBERS=src/api/subscribers/**/*Subscriber.ts 36 | RESOLVERS=src/api/resolvers/**/*Resolver.ts 37 | 38 | # 39 | # GraphQL 40 | # 41 | GRAPHQL_ENABLED=true 42 | GRAPHQL_ROUTE=/graphql 43 | GRAPHQL_EDITOR=false 44 | 45 | # 46 | # Swagger 47 | # 48 | SWAGGER_ENABLED=true 49 | SWAGGER_ROUTE=/swagger 50 | SWAGGER_USERNAME=admin 51 | SWAGGER_PASSWORD=1234 52 | 53 | # 54 | # Status Monitor 55 | # 56 | MONITOR_ENABLED=true 57 | MONITOR_ROUTE=/monitor 58 | MONITOR_USERNAME=admin 59 | MONITOR_PASSWORD=1234 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # @w3tec 2 | 3 | # Logs # 4 | /logs 5 | *.log 6 | *.log* 7 | 8 | # Node files # 9 | node_modules/ 10 | npm-debug.log 11 | yarn-error.log 12 | .env 13 | 14 | # OS generated files # 15 | .DS_Store 16 | Thumbs.db 17 | .tmp/ 18 | src/api/schema.gql 19 | 20 | # Typing # 21 | typings/ 22 | 23 | # Dist # 24 | dist/ 25 | tsconfig.build.json 26 | 27 | # IDE # 28 | .idea/ 29 | *.swp 30 | .awcache 31 | 32 | # Generated source-code # 33 | src/**/*.js 34 | src/**/*.js.map 35 | !src/public/**/* 36 | test/**/*.js 37 | test/**/*.js.map 38 | coverage/ 39 | !test/preprocessor.js 40 | mydb.sql 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.9.4" 4 | install: 5 | - yarn install 6 | env: 7 | - DB_TYPE="sqlite" DB_DATABASE="./mydb.sql" DB_LOGGING=false 8 | script: 9 | - npm start test 10 | - npm start test.integration 11 | - npm start test.e2e 12 | - npm start build 13 | notifications: 14 | email: false 15 | -------------------------------------------------------------------------------- /.vscode/cSpell.json: -------------------------------------------------------------------------------- 1 | // cSpell Settings 2 | { 3 | // Version of the setting file. Always 0.1 4 | "version": "0.1", 5 | // language - current active spelling language 6 | "language": "en", 7 | // words - list of words to be always considered correct 8 | "words": [ 9 | "hsts" 10 | ], 11 | // flagWords - list of words to be always considered incorrect 12 | // This is useful for offensive words and common spelling errors. 13 | // For example "hte" should be "the" 14 | "flagWords": [ 15 | "hte" 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker", 4 | "eg2.tslint", 5 | "EditorConfig.EditorConfig", 6 | "christian-kohler.path-intellisense", 7 | "mike-co.import-sorter", 8 | "mikestead.dotenv", 9 | "Orta.vscode-jest" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "${workspaceRoot}/dist/app.js", 12 | "smartStep": true, 13 | "outFiles": [ 14 | "../dist/**/*.js" 15 | ], 16 | "protocol": "inspector", 17 | "env": { 18 | "NODE_ENV": "production" 19 | } 20 | }, 21 | { 22 | "type": "node", 23 | "request": "attach", 24 | "name": "Nodemon Debug", 25 | "port": 9229, 26 | "restart": true 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "cSpell.enabled": true, 4 | "files.exclude": { 5 | "tsconfig.build.json": true, 6 | }, 7 | "importSorter.generalConfiguration.sortOnBeforeSave": true, 8 | "files.trimTrailingWhitespace": true, 9 | "editor.formatOnSave": false 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "npm", 4 | "isShellCommand": true, 5 | "suppressTaskName": true, 6 | "tasks": [ 7 | { 8 | // Build task, Cmd+Shift+B 9 | // "npm run build" 10 | "taskName": "build", 11 | "isBuildCommand": true, 12 | "args": [ 13 | "run", 14 | "build" 15 | ] 16 | }, 17 | { 18 | // Test task, Cmd+Shift+T 19 | // "npm test" 20 | "taskName": "test", 21 | "isTestCommand": true, 22 | "args": [ 23 | "test" 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at gery.hirschfeld@w3tec.ch & david.weber@w3tec.ch. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | # Create work directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install runtime dependencies 7 | RUN npm install yarn -g 8 | 9 | # Copy app source to work directory 10 | COPY . /usr/src/app 11 | 12 | # Install app dependencies 13 | RUN yarn install 14 | 15 | # Build and run the app 16 | CMD npm start serve 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 w3tecch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | w3tec 3 |

4 | 5 |

Express Typescript Boilerplate

6 | 7 |

8 | 9 | dependency 10 | 11 | 12 | travis 13 | 14 | 15 | appveyor 16 | 17 | 18 | StackShare 19 | 20 |

21 | 22 |

23 | A delightful way to building a Node.js RESTful API Services with beautiful code written in TypeScript.
24 | Inspired by the awesome framework laravel in PHP and of the repositories from pleerock
25 | Made with ❤️ by w3tech, Gery Hirschfeld and contributors 26 |

27 | 28 |
29 | 30 | ![divider](./w3tec-divider.png) 31 | 32 | ## ❯ Why 33 | 34 | Our main goal with this project is a feature complete server application. 35 | We like you to be focused on your business and not spending hours in project configuration. 36 | 37 | Try it!! We are happy to hear your feedback or any kind of new features. 38 | 39 | ### Features 40 | 41 | - **Beautiful Code** thanks to the awesome annotations of the libraries from [pleerock](https://github.com/pleerock). 42 | - **Easy API Testing** with included e2e testing. 43 | - **Dependency Injection** done with the nice framework from [TypeDI](https://github.com/pleerock/typedi). 44 | - **Simplified Database Query** with the ORM [TypeORM](https://github.com/typeorm/typeorm). 45 | - **Clear Structure** with different layers such as controllers, services, repositories, models, middlewares... 46 | - **Easy Exception Handling** thanks to [routing-controllers](https://github.com/pleerock/routing-controllers). 47 | - **Smart Validation** thanks to [class-validator](https://github.com/pleerock/class-validator) with some nice annotations. 48 | - **Custom Validators** to validate your request even better and stricter. [custom-validation-classes](https://github.com/pleerock/class-validator#custom-validation-classes). 49 | - **API Documentation** thanks to [swagger](http://swagger.io/) and [routing-controllers-openapi](https://github.com/epiphone/routing-controllers-openapi). 50 | - **API Monitoring** thanks to [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor). 51 | - **Integrated Testing Tool** thanks to [Jest](https://facebook.github.io/jest). 52 | - **E2E API Testing** thanks to [supertest](https://github.com/visionmedia/supertest). 53 | - **Basic Security Features** thanks to [Helmet](https://helmetjs.github.io/). 54 | - **Easy event dispatching** thanks to [event-dispatch](https://github.com/pleerock/event-dispatch). 55 | - **Fast Database Building** with simple migration from [TypeORM](https://github.com/typeorm/typeorm). 56 | - **Easy Data Seeding** with our own factories. 57 | - **GraphQL** provides as a awesome query language for our api [GraphQL](http://graphql.org/). 58 | - **TypeGraphQL** thanks to [TypeGraphQL](https://19majkel94.github.io/type-graphql/) we have a some cool decorators to simplify the usage of GraphQL. 59 | - **DataLoaders** helps with performance thanks to caching and batching [DataLoaders](https://github.com/facebook/dataloader). 60 | 61 | ![divider](./w3tec-divider.png) 62 | 63 | ## ❯ Table of Contents 64 | 65 | - [Getting Started](#-getting-started) 66 | - [Scripts and Tasks](#-scripts-and-tasks) 67 | - [Debugger in VSCode](#-debugger-in-vscode) 68 | - [API Routes](#-api-routes) 69 | - [Project Structure](#-project-structure) 70 | - [Logging](#-logging) 71 | - [Event Dispatching](#-event-dispatching) 72 | - [Seeding](#-seeding) 73 | - [GraphQL](#-graph-q-l) 74 | - [Docker](#-docker) 75 | - [Further Documentations](#-further-documentations) 76 | - [Related Projects](#-related-projects) 77 | - [License](#-license) 78 | 79 | ![divider](./w3tec-divider.png) 80 | 81 | ## ❯ Getting Started 82 | 83 | ### Step 1: Set up the Development Environment 84 | 85 | You need to set up your development environment before you can do anything. 86 | 87 | Install [Node.js and NPM](https://nodejs.org/en/download/) 88 | 89 | - on OSX use [homebrew](http://brew.sh) `brew install node` 90 | - on Windows use [chocolatey](https://chocolatey.org/) `choco install nodejs` 91 | 92 | Install yarn globally 93 | 94 | ```bash 95 | yarn global add yarn 96 | ``` 97 | 98 | Install a MySQL database. 99 | 100 | > If you work with a mac, we recommend to use homebrew for the installation. 101 | 102 | ### Step 2: Create new Project 103 | 104 | Fork or download this project. Configure your package.json for your new project. 105 | 106 | Then copy the `.env.example` file and rename it to `.env`. In this file you have to add your database connection information. 107 | 108 | Create a new database with the name you have in your `.env`-file. 109 | 110 | Then setup your application environment. 111 | 112 | ```bash 113 | yarn run setup 114 | ``` 115 | 116 | > This installs all dependencies with yarn. After that it migrates the database and seeds some test data into it. So after that your development environment is ready to use. 117 | 118 | ### Step 3: Serve your App 119 | 120 | Go to the project dir and start your app with this yarn script. 121 | 122 | ```bash 123 | yarn start serve 124 | ``` 125 | 126 | > This starts a local server using `nodemon`, which will watch for any file changes and will restart the server according to these changes. 127 | > The server address will be displayed to you as `http://0.0.0.0:3000`. 128 | 129 | ![divider](./w3tec-divider.png) 130 | 131 | ## ❯ Scripts and Tasks 132 | 133 | All script are defined in the `package-scripts.js` file, but the most important ones are listed here. 134 | 135 | ### Install 136 | 137 | - Install all dependencies with `yarn install` 138 | 139 | ### Linting 140 | 141 | - Run code quality analysis using `yarn start lint`. This runs tslint. 142 | - There is also a vscode task for this called `lint`. 143 | 144 | ### Tests 145 | 146 | - Run the unit tests using `yarn start test` (There is also a vscode task for this called `test`). 147 | - Run the integration tests using `yarn start test.integration`. 148 | - Run the e2e tests using `yarn start test.e2e`. 149 | 150 | ### Running in dev mode 151 | 152 | - Run `yarn start serve` to start nodemon with ts-node, to serve the app. 153 | - The server address will be displayed to you as `http://0.0.0.0:3000` 154 | 155 | ### Building the project and run it 156 | 157 | - Run `yarn start build` to generated all JavaScript files from the TypeScript sources (There is also a vscode task for this called `build`). 158 | - To start the builded app located in `dist` use `yarn start`. 159 | 160 | ### Database Migration 161 | 162 | - Run `typeorm migration:create -n ` to create a new migration file. 163 | - Try `typeorm -h` to see more useful cli commands like generating migration out of your models. 164 | - To migrate your database run `yarn start db.migrate`. 165 | - To revert your latest migration run `yarn start db.revert`. 166 | - Drops the complete database schema `yarn start db.drop`. 167 | 168 | ### Database Seeding 169 | 170 | - Run `yarn start db.seed` to seed your seeds into the database. 171 | 172 | ![divider](./w3tec-divider.png) 173 | 174 | ## ❯ Debugger in VSCode 175 | 176 | To debug your code run `yarn start build` or hit cmd + b to build your app. 177 | Then, just set a breakpoint and hit F5 in your Visual Studio Code. 178 | 179 | ![divider](./w3tec-divider.png) 180 | 181 | ## ❯ API Routes 182 | 183 | The route prefix is `/api` by default, but you can change this in the .env file. 184 | The swagger and the monitor route can be altered in the `.env` file. 185 | 186 | | Route | Description | 187 | | -------------- | ----------- | 188 | | **/api** | Shows us the name, description and the version of the package.json | 189 | | **/graphql** | Route to the graphql editor or your query/mutations requests | 190 | | **/swagger** | This is the Swagger UI with our API documentation | 191 | | **/monitor** | Shows a small monitor page for the server | 192 | | **/api/users** | Example entity endpoint | 193 | | **/api/pets** | Example entity endpoint | 194 | 195 | ![divider](./w3tec-divider.png) 196 | 197 | ## ❯ Project Structure 198 | 199 | | Name | Description | 200 | | --------------------------------- | ----------- | 201 | | **.vscode/** | VSCode tasks, launch configuration and some other settings | 202 | | **dist/** | Compiled source files will be placed here | 203 | | **src/** | Source files | 204 | | **src/api/controllers/** | REST API Controllers | 205 | | **src/api/controllers/requests** | Request classes with validation rules if the body is not equal with a model | 206 | | **src/api/controllers/responses** | Response classes or interfaces to type json response bodies | 207 | | **src/api/errors/** | Custom HttpErrors like 404 NotFound | 208 | | **src/api/interceptors/** | Interceptors are used to change or replace the data returned to the client. | 209 | | **src/api/middlewares/** | Express Middlewares like helmet security features | 210 | | **src/api/models/** | TypeORM Models | 211 | | **src/api/repositories/** | Repository / DB layer | 212 | | **src/api/services/** | Service layer | 213 | | **src/api/subscribers/** | Event subscribers | 214 | | **src/api/validators/** | Custom validators, which can be used in the request classes | 215 | | **src/api/resolvers/** | GraphQL resolvers (query, mutation & field-resolver) | 216 | | **src/api/types/** | GraphQL types ,input-types and scalar types | 217 | | **src/api/** schema.gql | Generated GraphQL schema | 218 | | **src/auth/** | Authentication checkers and services | 219 | | **src/core/** | The core features like logger and env variables | 220 | | **src/database/factories** | Factory the generate fake entities | 221 | | **src/database/migrations** | Database migration scripts | 222 | | **src/database/seeds** | Seeds to create some data in the database | 223 | | **src/decorators/** | Custom decorators like @Logger & @EventDispatch | 224 | | **src/loaders/** | Loader is a place where you can configure your app | 225 | | **src/public/** | Static assets (fonts, css, js, img). | 226 | | **src/types/** *.d.ts | Custom type definitions and files that aren't on DefinitelyTyped | 227 | | **test** | Tests | 228 | | **test/e2e/** *.test.ts | End-2-End tests (like e2e) | 229 | | **test/integration/** *.test.ts | Integration test with SQLite3 | 230 | | **test/unit/** *.test.ts | Unit tests | 231 | | .env.example | Environment configurations | 232 | | .env.test | Test environment configurations | 233 | | mydb.sql | SQLite database for integration tests. Ignored by git and only available after integration tests | 234 | 235 | ![divider](./w3tec-divider.png) 236 | 237 | ## ❯ Logging 238 | 239 | Our logger is [winston](https://github.com/winstonjs/winston). To log http request we use the express middleware [morgan](https://github.com/expressjs/morgan). 240 | We created a simple annotation to inject the logger in your service (see example below). 241 | 242 | ```typescript 243 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 244 | 245 | @Service() 246 | export class UserService { 247 | 248 | constructor( 249 | @Logger(__filename) private log: LoggerInterface 250 | ) { } 251 | 252 | ... 253 | ``` 254 | 255 | ![divider](./w3tec-divider.png) 256 | 257 | ## ❯ Event Dispatching 258 | 259 | We use this awesome repository [event-dispatch](https://github.com/pleerock/event-dispatch) for event dispatching. 260 | We created a simple annotation to inject the EventDispatcher in your service (see example below). All events are listed in the `events.ts` file. 261 | 262 | ```typescript 263 | import { events } from '../subscribers/events'; 264 | import { EventDispatcher, EventDispatcherInterface } from '../../decorators/EventDispatcher'; 265 | 266 | @Service() 267 | export class UserService { 268 | 269 | constructor( 270 | @EventDispatcher() private eventDispatcher: EventDispatcherInterface 271 | ) { } 272 | 273 | public async create(user: User): Promise { 274 | ... 275 | this.eventDispatcher.dispatch(events.user.created, newUser); 276 | ... 277 | } 278 | ``` 279 | 280 | ![divider](./w3tec-divider.png) 281 | 282 | ## ❯ Seeding 283 | 284 | Isn't it exhausting to create some sample data for your database, well this time is over! 285 | 286 | How does it work? Just create a factory for your entities (models) and a seed script. 287 | 288 | ### 1. Create a factory for your entity 289 | 290 | For all entities we want to seed, we need to define a factory. To do so we give you the awesome [faker](https://github.com/marak/Faker.js/) library as a parameter into your factory. Then create your "fake" entity and return it. Those factory files should be in the `src/database/factories` folder and suffixed with `Factory` like `src/database/factories/UserFactory.ts`. 291 | 292 | Settings can be used to pass some static value into the factory. 293 | 294 | ```typescript 295 | define(User, (faker: typeof Faker, settings: { roles: string[] }) => { 296 | const gender = faker.random.number(1); 297 | const firstName = faker.name.firstName(gender); 298 | const lastName = faker.name.lastName(gender); 299 | const email = faker.internet.email(firstName, lastName); 300 | 301 | const user = new User(); 302 | user.firstName = firstName; 303 | user.lastName = lastName; 304 | user.email = email; 305 | user.roles = settings.roles; 306 | return user; 307 | }); 308 | ``` 309 | 310 | Handle relation in the entity factory like this. 311 | 312 | ```typescript 313 | define(Pet, (faker: typeof Faker, settings: undefined) => { 314 | const gender = faker.random.number(1); 315 | const name = faker.name.firstName(gender); 316 | 317 | const pet = new Pet(); 318 | pet.name = name; 319 | pet.age = faker.random.number(); 320 | pet.user = factory(User)({ roles: ['admin'] }) 321 | return pet; 322 | }); 323 | ``` 324 | 325 | ### 2. Create a seed file 326 | 327 | The seeds files define how much and how the data are connected with each other. The files will be executed alphabetically. 328 | With the second function, accepting your settings defined in the factories, you are able to create different variations of entities. 329 | 330 | ```typescript 331 | export class CreateUsers implements Seed { 332 | 333 | public async seed(factory: Factory, connection: Connection): Promise { 334 | await factory(User)({ roles: [] }).createMany(10); 335 | } 336 | 337 | } 338 | ``` 339 | 340 | Here an example with nested factories. You can use the `.map()` function to alter 341 | the generated value before they get persisted. 342 | 343 | ```typescript 344 | ... 345 | await factory(User)() 346 | .map(async (user: User) => { 347 | const pets: Pet[] = await factory(Pet)().createMany(2); 348 | const petIds = pets.map((pet: Pet) => pet.Id); 349 | await user.pets().attach(petIds); 350 | }) 351 | .createMany(5); 352 | ... 353 | ``` 354 | 355 | To deal with relations you can use the entity manager like this. 356 | 357 | ```typescript 358 | export class CreatePets implements SeedsInterface { 359 | 360 | public async seed(factory: FactoryInterface, connection: Connection): Promise { 361 | const connection = await factory.getConnection(); 362 | const em = connection.createEntityManager(); 363 | 364 | await times(10, async (n) => { 365 | // This creates a pet in the database 366 | const pet = await factory(Pet)().create(); 367 | // This only returns a entity with fake data 368 | const user = await factory(User)({ roles: ['admin'] }).make(); 369 | user.pets = [pet]; 370 | await em.save(user); 371 | }); 372 | } 373 | 374 | } 375 | ``` 376 | 377 | ### 3. Run the seeder 378 | 379 | The last step is the easiest, just hit the following command in your terminal, but be sure you are in the projects root folder. 380 | 381 | ```bash 382 | yarn start db.seed 383 | ``` 384 | 385 | #### CLI Interface 386 | 387 | | Command | Description | 388 | | ---------------------------------------------------- | ----------- | 389 | | `yarn start "db.seed"` | Run all seeds | 390 | | `yarn start "db.seed --run CreateBruce,CreatePets"` | Run specific seeds (file names without extension) | 391 | | `yarn start "db.seed -L"` | Log database queries to the terminal | 392 | | `yarn start "db.seed --factories "` | Add a different path to your factories (Default: `src/database/`) | 393 | | `yarn start "db.seed --seeds "` | Add a different path to your seeds (Default: `src/database/seeds/`) | 394 | 395 | ![divider](./w3tec-divider.png) 396 | 397 | ## ❯ GraphQL 398 | 399 | For the GraphQL part we used the library [TypeGraphQL](https://19majkel94.github.io/type-graphql/) to build awesome GraphQL API's. 400 | 401 | The context(shown below) of the GraphQL is builded in the **graphqlLoader.ts** file. Inside of this loader we create a scoped container for each incoming request. 402 | 403 | ```typescript 404 | export interface Context { 405 | requestId: number; 406 | request: express.Request; 407 | response: express.Response; 408 | container: ContainerInstance; 409 | } 410 | ``` 411 | 412 | ### DataLoader 413 | 414 | For the usage of the DataLoaders we created a annotation, which automatically creates and registers a new DataLoader to the scoped container. 415 | 416 | Here is an example of the **PetResolver**. 417 | 418 | ```typescript 419 | import DataLoader from 'dataloader'; 420 | import { DLoader } from '../../decorators/DLoader'; 421 | ... 422 | constructor( 423 | private petService: PetService, 424 | @Logger(__filename) private log: LoggerInterface, 425 | @DLoader(UserModel) private userLoader: DataLoader 426 | ) { } 427 | ... 428 | ``` 429 | 430 | Or you could use the repository too. 431 | 432 | ```typescript 433 | @DLoader(UserRepository) private userLoader: DataLoader 434 | ``` 435 | 436 | Or even use a custom method of your given repository. 437 | 438 | ```typescript 439 | @DLoader(PetRepository, { 440 | method: 'findByUserIds', 441 | key: 'userId', 442 | multiple: true, 443 | }) private petLoader: DataLoader 444 | ``` 445 | 446 | ## ❯ Docker 447 | 448 | ### Install Docker 449 | 450 | Before you start, make sure you have a recent version of [Docker](https://docs.docker.com/engine/installation/) installed 451 | 452 | ### Build Docker image 453 | 454 | ```shell 455 | docker build -t . 456 | ``` 457 | 458 | ### Run Docker image in container and map port 459 | 460 | The port which runs your application inside Docker container is either configured as `PORT` property in your `.env` configuration file or passed to Docker container via environment variable `PORT`. Default port is `3000`. 461 | 462 | #### Run image in detached mode 463 | 464 | ```shell 465 | docker run -d -p : 466 | ``` 467 | 468 | #### Run image in foreground mode 469 | 470 | ```shell 471 | docker run -i -t -p : 472 | ``` 473 | 474 | ### Stop Docker container 475 | 476 | #### Detached mode 477 | 478 | ```shell 479 | docker stop 480 | ``` 481 | 482 | You can get a list of all running Docker container and its ids by following command 483 | 484 | ```shell 485 | docker images 486 | ``` 487 | 488 | #### Foreground mode 489 | 490 | Go to console and press + C at any time. 491 | 492 | ### Docker environment variables 493 | 494 | There are several options to configure your app inside a Docker container 495 | 496 | #### project .env file 497 | 498 | You can use `.env` file in project root folder which will be copied inside Docker image. If you want to change a property inside `.env` you have to rebuild your Docker image. 499 | 500 | #### run options 501 | 502 | You can also change app configuration by passing environment variables via `docker run` option `-e` or `--env`. 503 | 504 | ```shell 505 | docker run --env DB_HOST=localhost -e DB_PORT=3306 506 | ``` 507 | 508 | #### environment file 509 | 510 | Last but not least you can pass a config file to `docker run`. 511 | 512 | ```shell 513 | docker run --env-file ./env.list 514 | ``` 515 | 516 | `env.list` example: 517 | 518 | ``` 519 | # this is a comment 520 | DB_TYPE=mysql 521 | DB_HOST=localhost 522 | DB_PORT=3306 523 | ``` 524 | 525 | ![divider](./w3tec-divider.png) 526 | 527 | ## ❯ Further Documentations 528 | 529 | | Name & Link | Description | 530 | | --------------------------------- | --------------------------------- | 531 | | [Express](https://expressjs.com/) | Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. | 532 | | [Microframework](https://github.com/pleerock/microframework) | Microframework is a simple tool that allows you to execute your modules in a proper order, helping you to organize bootstrap code in your application. | 533 | | [TypeDI](https://github.com/pleerock/typedi) | Dependency Injection for TypeScript. | 534 | | [routing-controllers](https://github.com/pleerock/routing-controllers) | Create structured, declarative and beautifully organized class-based controllers with heavy decorators usage in Express / Koa using TypeScript and Routing Controllers Framework. | 535 | | [TypeORM](http://typeorm.io/#/) | TypeORM is highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework. | 536 | | [class-validator](https://github.com/pleerock/class-validator) | Validation made easy using TypeScript decorators. | 537 | | [class-transformer](https://github.com/pleerock/class-transformer) | Proper decorator-based transformation / serialization / deserialization of plain javascript objects to class constructors | 538 | | [event-dispatcher](https://github.com/pleerock/event-dispatch) | Dispatching and listening for application events in Typescript | 539 | | [Helmet](https://helmetjs.github.io/) | Helmet helps you secure your Express apps by setting various HTTP headers. It’s not a silver bullet, but it can help! | 540 | | [Auth0 API Documentation](https://auth0.com/docs/api/management/v2) | Authentification service | 541 | | [Jest](http://facebook.github.io/jest/) | Delightful JavaScript Testing Library for unit and e2e tests | 542 | | [supertest](https://github.com/visionmedia/supertest) | Super-agent driven library for testing node.js HTTP servers using a fluent API | 543 | | [nock](https://github.com/node-nock/nock) | HTTP mocking and expectations library | 544 | | [swagger Documentation](http://swagger.io/) | API Tool to describe and document your api. | 545 | | [SQLite Documentation](https://www.sitepoint.com/getting-started-sqlite3-basic-commands/) | Getting Started with SQLite3 – Basic Commands. | 546 | | [GraphQL Documentation](http://graphql.org/graphql-js/) | A query language for your API. | 547 | | [DataLoader Documentation](https://github.com/facebook/dataloader) | DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching. | 548 | 549 | ![divider](./w3tec-divider.png) 550 | 551 | ## ❯ Related Projects 552 | 553 | - [Microsoft/TypeScript-Node-Starter](https://github.com/Microsoft/TypeScript-Node-Starter) - A starter template for TypeScript and Node with a detailed README describing how to use the two together. 554 | - [express-graphql-typescript-boilerplate](https://github.com/w3tecch/express-graphql-typescript-boilerplate) - A starter kit for building amazing GraphQL API's with TypeScript and express by @w3tecch 555 | - [aurelia-typescript-boilerplate](https://github.com/w3tecch/aurelia-typescript-boilerplate) - An Aurelia starter kit with TypeScript 556 | - [Auth0 Mock Server](https://github.com/hirsch88/auth0-mock-server) - Useful for e2e testing or faking an oAuth server 557 | 558 | ![divider](./w3tec-divider.png) 559 | 560 | ## ❯ License 561 | 562 | [MIT](/LICENSE) 563 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "8" 3 | DB_TYPE: "sqlite" 4 | DB_DATABASE: "./mydb.sql" 5 | DB_LOGGING: false 6 | 7 | install: 8 | - ps: Install-Product node $env:nodejs_version 9 | - yarn install 10 | 11 | build_script: 12 | - npm start build 13 | 14 | test_script: 15 | - npm start test 16 | - npm start test.integration 17 | - npm start test.e2e 18 | -------------------------------------------------------------------------------- /commands/banner.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as figlet from 'figlet'; 3 | 4 | figlet.text(process.argv[2], (error: any, data: any) => { 5 | if (error) { 6 | return process.exit(1); 7 | } 8 | 9 | console.log(chalk.blue(data)); 10 | console.log(''); 11 | return process.exit(0); 12 | }); 13 | -------------------------------------------------------------------------------- /commands/seed.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import commander from 'commander'; 3 | import * as path from 'path'; 4 | import { 5 | loadConnection, loadEntityFactories, loadSeeds, runSeed, setConnection 6 | } from 'typeorm-seeding'; 7 | 8 | // Cli helper 9 | commander 10 | .version('1.0.0') 11 | .description('Run database seeds of your project') 12 | .option('-L, --logging', 'enable sql query logging') 13 | .option('--factories ', 'add filepath for your factories') 14 | .option('--seeds ', 'add filepath for your seeds') 15 | .option('--run ', 'run specific seeds (file names without extension)', (val) => val.split(',')) 16 | .option('--config ', 'path to your ormconfig.json file (must be a json)') 17 | .parse(process.argv); 18 | 19 | // Get cli parameter for a different factory path 20 | const factoryPath = (commander.factories) 21 | ? commander.factories 22 | : 'src/database/factories'; 23 | 24 | // Get cli parameter for a different seeds path 25 | const seedsPath = (commander.seeds) 26 | ? commander.seeds 27 | : 'src/database/seeds/'; 28 | 29 | // Get a list of seeds 30 | const listOfSeeds = (commander.run) 31 | ? commander.run.map(l => l.trim()).filter(l => l.length > 0) 32 | : []; 33 | 34 | // Search for seeds and factories 35 | const run = async () => { 36 | const log = console.log; 37 | 38 | let factoryFiles; 39 | let seedFiles; 40 | try { 41 | factoryFiles = await loadEntityFactories(factoryPath); 42 | seedFiles = await loadSeeds(seedsPath); 43 | } catch (error) { 44 | return handleError(error); 45 | } 46 | 47 | // Filter seeds 48 | if (listOfSeeds.length > 0) { 49 | seedFiles = seedFiles.filter(sf => listOfSeeds.indexOf(path.basename(sf).replace('.ts', '')) >= 0); 50 | } 51 | 52 | // Status logging to print out the amount of factories and seeds. 53 | log(chalk.bold('seeds')); 54 | log('🔎 ', chalk.gray.underline(`found:`), 55 | chalk.blue.bold(`${factoryFiles.length} factories`, chalk.gray('&'), chalk.blue.bold(`${seedFiles.length} seeds`))); 56 | 57 | // Get database connection and pass it to the seeder 58 | try { 59 | const connection = await loadConnection(); 60 | setConnection(connection); 61 | } catch (error) { 62 | return handleError(error); 63 | } 64 | 65 | // Show seeds in the console 66 | for (const seedFile of seedFiles) { 67 | try { 68 | let className = seedFile.split('/')[seedFile.split('/').length - 1]; 69 | className = className.replace('.ts', '').replace('.js', ''); 70 | className = className.split('-')[className.split('-').length - 1]; 71 | log('\n' + chalk.gray.underline(`executing seed: `), chalk.green.bold(`${className}`)); 72 | const seedFileObject: any = require(seedFile); 73 | await runSeed(seedFileObject[className]); 74 | } catch (error) { 75 | console.error('Could not run seed ', error); 76 | process.exit(1); 77 | } 78 | } 79 | 80 | log('\n👍 ', chalk.gray.underline(`finished seeding`)); 81 | process.exit(0); 82 | }; 83 | 84 | const handleError = (error) => { 85 | console.error(error); 86 | process.exit(1); 87 | }; 88 | 89 | run(); 90 | -------------------------------------------------------------------------------- /commands/tsconfig.ts: -------------------------------------------------------------------------------- 1 | import * as jsonfile from 'jsonfile'; 2 | import * as path from 'path'; 3 | 4 | import * as tsconfig from '../tsconfig.json'; 5 | 6 | const content: any = tsconfig; 7 | content.compilerOptions.outDir = '.tmp'; 8 | content.include = [ 9 | 'src/**/*', 10 | ]; 11 | 12 | const filePath = path.join(process.cwd(), 'tsconfig.build.json'); 13 | jsonfile.writeFile(filePath, content, { spaces: 2 }, (err) => { 14 | if (err === null) { 15 | process.exit(0); 16 | } else { 17 | console.error('Failed to generate the tsconfig.build.json', err); 18 | process.exit(1); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tecch/express-typescript-boilerplate/17727010f7f098d8ca8b78da0a5b3c7297659381/icon.png -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": "0", 3 | "execMap": { 4 | "ts": "node -r ts-node/register" 5 | }, 6 | "events": { 7 | "start": "tslint -c ./tslint.json -t stylish 'src/**/*.ts'" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package-scripts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Windows: Please do not use trailing comma as windows will fail with token error 3 | */ 4 | 5 | const { series, rimraf, } = require('nps-utils'); 6 | 7 | module.exports = { 8 | scripts: { 9 | default: 'nps start', 10 | /** 11 | * Starts the builded app from the dist directory. 12 | */ 13 | start: { 14 | script: 'cross-env NODE_ENV=production node dist/app.js', 15 | description: 'Starts the builded app', 16 | }, 17 | /** 18 | * Serves the current app and watches for changes to restart it 19 | */ 20 | serve: { 21 | inspector: { 22 | script: series( 23 | 'nps banner.serve', 24 | 'nodemon --watch src --watch .env --inspect' 25 | ), 26 | description: 'Serves the current app and watches for changes to restart it, you may attach inspector to it.' 27 | }, 28 | script: series( 29 | 'nps banner.serve', 30 | 'nodemon --watch src --watch .env' 31 | ), 32 | description: 'Serves the current app and watches for changes to restart it' 33 | }, 34 | /** 35 | * Setup of the development environment 36 | */ 37 | setup: { 38 | script: series( 39 | 'yarn install', 40 | 'nps db.setup', 41 | ), 42 | description: 'Setup`s the development environment(yarn & database)' 43 | }, 44 | /** 45 | * Creates the needed configuration files 46 | */ 47 | config: { 48 | script: series( 49 | runFast('./commands/tsconfig.ts'), 50 | ), 51 | hiddenFromHelp: true 52 | }, 53 | /** 54 | * Builds the app into the dist directory 55 | */ 56 | build: { 57 | script: series( 58 | 'nps banner.build', 59 | 'nps config', 60 | 'nps lint', 61 | 'nps clean.dist', 62 | 'nps transpile', 63 | 'nps copy', 64 | 'nps copy.tmp', 65 | 'nps clean.tmp', 66 | ), 67 | description: 'Builds the app into the dist directory' 68 | }, 69 | /** 70 | * Runs TSLint over your project 71 | */ 72 | lint: { 73 | script: tslint(`./src/**/*.ts`), 74 | hiddenFromHelp: true 75 | }, 76 | /** 77 | * Transpile your app into javascript 78 | */ 79 | transpile: { 80 | script: `tsc --project ./tsconfig.build.json`, 81 | hiddenFromHelp: true 82 | }, 83 | /** 84 | * Clean files and folders 85 | */ 86 | clean: { 87 | default: { 88 | script: series( 89 | `nps banner.clean`, 90 | `nps clean.dist` 91 | ), 92 | description: 'Deletes the ./dist folder' 93 | }, 94 | dist: { 95 | script: rimraf('./dist'), 96 | hiddenFromHelp: true 97 | }, 98 | tmp: { 99 | script: rimraf('./.tmp'), 100 | hiddenFromHelp: true 101 | } 102 | }, 103 | /** 104 | * Copies static files to the build folder 105 | */ 106 | copy: { 107 | default: { 108 | script: series( 109 | `nps copy.public` 110 | ), 111 | hiddenFromHelp: true 112 | }, 113 | public: { 114 | script: copy( 115 | './src/public/*', 116 | './dist' 117 | ), 118 | hiddenFromHelp: true 119 | }, 120 | tmp: { 121 | script: copyDir( 122 | './.tmp/src', 123 | './dist' 124 | ), 125 | hiddenFromHelp: true 126 | } 127 | }, 128 | /** 129 | * Database scripts 130 | */ 131 | db: { 132 | migrate: { 133 | script: series( 134 | 'nps banner.migrate', 135 | 'nps config', 136 | runFast('./node_modules/typeorm/cli.js migration:run') 137 | ), 138 | description: 'Migrates the database to newest version available' 139 | }, 140 | revert: { 141 | script: series( 142 | 'nps banner.revert', 143 | 'nps config', 144 | runFast('./node_modules/typeorm/cli.js migration:revert') 145 | ), 146 | description: 'Downgrades the database' 147 | }, 148 | seed: { 149 | script: series( 150 | 'nps banner.seed', 151 | 'nps config', 152 | runFast('./commands/seed.ts') 153 | ), 154 | description: 'Seeds generated records into the database' 155 | }, 156 | drop: { 157 | script: runFast('./node_modules/typeorm/cli.js schema:drop'), 158 | description: 'Drops the schema of the database' 159 | }, 160 | setup: { 161 | script: series( 162 | 'nps db.drop', 163 | 'nps db.migrate', 164 | 'nps db.seed' 165 | ), 166 | description: 'Recreates the database with seeded data' 167 | } 168 | }, 169 | /** 170 | * These run various kinds of tests. Default is unit. 171 | */ 172 | test: { 173 | default: 'nps test.unit', 174 | unit: { 175 | default: { 176 | script: series( 177 | 'nps banner.testUnit', 178 | 'nps test.unit.pretest', 179 | 'nps test.unit.run' 180 | ), 181 | description: 'Runs the unit tests' 182 | }, 183 | pretest: { 184 | script: tslint(`./test/unit/**.ts`), 185 | hiddenFromHelp: true 186 | }, 187 | run: { 188 | script: 'cross-env NODE_ENV=test jest --testPathPattern=unit', 189 | hiddenFromHelp: true 190 | }, 191 | verbose: { 192 | script: 'nps "test --verbose"', 193 | hiddenFromHelp: true 194 | }, 195 | coverage: { 196 | script: 'nps "test --coverage"', 197 | hiddenFromHelp: true 198 | } 199 | }, 200 | integration: { 201 | default: { 202 | script: series( 203 | 'nps banner.testIntegration', 204 | 'nps test.integration.pretest', 205 | 'nps test.integration.run' 206 | ), 207 | description: 'Runs the integration tests' 208 | }, 209 | pretest: { 210 | script: tslint(`./test/integration/**.ts`), 211 | hiddenFromHelp: true 212 | }, 213 | run: { 214 | // -i. Run all tests serially in the current process, rather than creating a worker pool of child processes that run tests. This can be useful for debugging. 215 | script: 'cross-env NODE_ENV=test jest --testPathPattern=integration -i', 216 | hiddenFromHelp: true 217 | }, 218 | verbose: { 219 | script: 'nps "test --verbose"', 220 | hiddenFromHelp: true 221 | }, 222 | coverage: { 223 | script: 'nps "test --coverage"', 224 | hiddenFromHelp: true 225 | } 226 | }, 227 | e2e: { 228 | default: { 229 | script: series( 230 | 'nps banner.testE2E', 231 | 'nps test.e2e.pretest', 232 | 'nps test.e2e.run' 233 | ), 234 | description: 'Runs the e2e tests' 235 | }, 236 | pretest: { 237 | script: tslint(`./test/e2e/**.ts`), 238 | hiddenFromHelp: true 239 | }, 240 | run: { 241 | // -i. Run all tests serially in the current process, rather than creating a worker pool of child processes that run tests. This can be useful for debugging. 242 | script: 'cross-env NODE_ENV=test jest --testPathPattern=e2e -i', 243 | hiddenFromHelp: true 244 | }, 245 | verbose: { 246 | script: 'nps "test --verbose"', 247 | hiddenFromHelp: true 248 | }, 249 | coverage: { 250 | script: 'nps "test --coverage"', 251 | hiddenFromHelp: true 252 | } 253 | }, 254 | }, 255 | /** 256 | * This creates pretty banner to the terminal 257 | */ 258 | banner: { 259 | build: banner('build'), 260 | serve: banner('serve'), 261 | testUnit: banner('test.unit'), 262 | testIntegration: banner('test.integration'), 263 | testE2E: banner('test.e2e'), 264 | migrate: banner('migrate'), 265 | seed: banner('seed'), 266 | revert: banner('revert'), 267 | clean: banner('clean') 268 | } 269 | } 270 | }; 271 | 272 | function banner(name) { 273 | return { 274 | hiddenFromHelp: true, 275 | silent: true, 276 | description: `Shows ${name} banners to the console`, 277 | script: runFast(`./commands/banner.ts ${name}`), 278 | }; 279 | } 280 | 281 | function copy(source, target) { 282 | return `copyfiles --up 1 ${source} ${target}`; 283 | } 284 | 285 | function copyDir(source, target) { 286 | return `ncp ${source} ${target}`; 287 | } 288 | 289 | function run(path) { 290 | return `ts-node ${path}`; 291 | } 292 | 293 | function runFast(path) { 294 | return `ts-node --transpile-only ${path}`; 295 | } 296 | 297 | function tslint(path) { 298 | return `tslint -c ./tslint.json ${path} --format stylish`; 299 | } 300 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-typescript-boilerplate", 3 | "version": "3.2.0", 4 | "description": "A delightful way to building a Node.js RESTful API Services with beautiful code written in TypeScript", 5 | "main": "src/app.ts", 6 | "scripts": { 7 | "start": "nps", 8 | "test": "npm start test", 9 | "build": "npm start build", 10 | "presetup": "yarn install", 11 | "setup": "npm start config && npm start setup.script" 12 | }, 13 | "engines": { 14 | "node": ">=8.0.0" 15 | }, 16 | "repository": "git+ssh://git@github.com/w3tec/express-typescript-boilerplate.git", 17 | "keywords": [ 18 | "NodeJS", 19 | "TypeScript", 20 | "express", 21 | "boilerplate", 22 | "skeleton", 23 | "starter-kit", 24 | "w3tec.ch" 25 | ], 26 | "homepage": "https://github.com/w3tec/express-typescript-boilerplate#readme", 27 | "author": "w3tec.ch ", 28 | "contributors": [ 29 | { 30 | "name": "David Weber", 31 | "email": "david.weber@w3tec.ch", 32 | "url": "https://github.com/dweber019" 33 | }, 34 | { 35 | "name": "Gery Hirschfeld", 36 | "email": "gery.hirschfeld@w3tec.ch", 37 | "url": "https://github.com/hirsch88" 38 | } 39 | ], 40 | "dependencies": { 41 | "bcrypt": "3.0.1", 42 | "chalk": "^2.4.1", 43 | "class-validator": "^0.9.1", 44 | "class-validator-jsonschema": "^1.3.0", 45 | "commander": "^2.19.0", 46 | "compression": "^1.7.1", 47 | "copyfiles": "^2.1.0", 48 | "cors": "^2.8.4", 49 | "dataloader": "^1.3.0", 50 | "dotenv": "6.0.0", 51 | "event-dispatch": "^0.4.1", 52 | "express": "^4.16.2", 53 | "express-basic-auth": "^1.1.3", 54 | "express-graphql": "^0.6.11", 55 | "express-status-monitor": "^1.0.1", 56 | "faker": "^4.1.0", 57 | "figlet": "^1.2.0", 58 | "glob": "^7.1.2", 59 | "graphql": "14.0.2", 60 | "helmet": "^3.9.0", 61 | "jsonfile": "5.0.0", 62 | "microframework-w3tec": "^0.6.3", 63 | "morgan": "^1.9.0", 64 | "mysql": "^2.16.0", 65 | "nodemon": "^1.12.1", 66 | "nps": "^5.9.3", 67 | "nps-utils": "^1.5.0", 68 | "pg": "^7.4.3", 69 | "reflect-metadata": "^0.1.10", 70 | "routing-controllers": "^0.7.6", 71 | "routing-controllers-openapi": "^1.7.0", 72 | "serve-favicon": "^2.4.5", 73 | "supertest": "^3.0.0", 74 | "swagger-ui-express": "4.0.1", 75 | "ts-node": "7.0.1", 76 | "tslint": "^5.8.0", 77 | "type-graphql": "^0.15.0", 78 | "typedi": "0.8.0", 79 | "typeorm": "^0.2.5", 80 | "typeorm-seeding": "^1.0.0-beta.6", 81 | "typeorm-typedi-extensions": "^0.2.1", 82 | "typescript": "^3.6.3", 83 | "uuid": "^3.3.2", 84 | "winston": "3.1.0" 85 | }, 86 | "resolutions": { 87 | "**/event-stream": "^4.0.1" 88 | }, 89 | "jest": { 90 | "transform": { 91 | ".(ts|tsx)": "/test/preprocessor.js" 92 | }, 93 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 94 | "moduleFileExtensions": [ 95 | "ts", 96 | "tsx", 97 | "js", 98 | "json" 99 | ], 100 | "testEnvironment": "node", 101 | "setupTestFrameworkScriptFile": "./test/unit/lib/setup.ts" 102 | }, 103 | "license": "MIT", 104 | "devDependencies": { 105 | "@types/bcrypt": "^2.0.0", 106 | "@types/bluebird": "^3.5.18", 107 | "@types/chalk": "^2.2.0", 108 | "@types/commander": "^2.11.0", 109 | "@types/cors": "^2.8.1", 110 | "@types/dotenv": "^4.0.2", 111 | "@types/express": "^4.0.39", 112 | "@types/faker": "^4.1.2", 113 | "@types/figlet": "^1.2.0", 114 | "@types/helmet": "0.0.41", 115 | "@types/jest": "23.3.2", 116 | "@types/morgan": "^1.7.35", 117 | "@types/nock": "^9.1.3", 118 | "@types/node": "^12.7.5", 119 | "@types/reflect-metadata": "0.1.0", 120 | "@types/serve-favicon": "^2.2.29", 121 | "@types/supertest": "^2.0.4", 122 | "@types/uuid": "^3.4.3", 123 | "@types/winston": "^2.3.7", 124 | "cross-env": "^5.1.1", 125 | "jest": "23.6.0", 126 | "mock-express-request": "^0.2.0", 127 | "mock-express-response": "^0.2.1", 128 | "ncp": "^2.0.0", 129 | "nock": "10.0.0", 130 | "sqlite3": "^4.0.0", 131 | "ts-jest": "23.10.1" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/api/Context.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { ContainerInstance } from 'typedi'; 3 | 4 | export interface Context { 5 | requestId: number; 6 | request: express.Request; 7 | response: express.Response; 8 | container: ContainerInstance; 9 | } 10 | -------------------------------------------------------------------------------- /src/api/controllers/PetController.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumber, IsUUID, ValidateNested } from 'class-validator'; 2 | import { 3 | Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put 4 | } from 'routing-controllers'; 5 | import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; 6 | 7 | import { PetNotFoundError } from '../errors/PetNotFoundError'; 8 | import { Pet } from '../models/Pet'; 9 | import { PetService } from '../services/PetService'; 10 | import { UserResponse } from './UserController'; 11 | 12 | class BasePet { 13 | @IsNotEmpty() 14 | public name: string; 15 | 16 | @IsNumber() 17 | public age: number; 18 | } 19 | 20 | export class PetResponse extends BasePet { 21 | @IsUUID() 22 | public id: string; 23 | 24 | @ValidateNested() 25 | public user: UserResponse; 26 | } 27 | 28 | class CreatePetBody extends BasePet { 29 | @IsUUID() 30 | public userId: string; 31 | } 32 | 33 | @Authorized() 34 | @JsonController('/pets') 35 | @OpenAPI({ security: [{ basicAuth: [] }] }) 36 | export class PetController { 37 | 38 | constructor( 39 | private petService: PetService 40 | ) { } 41 | 42 | @Get() 43 | @ResponseSchema(PetResponse, { isArray: true }) 44 | public find(): Promise { 45 | return this.petService.find(); 46 | } 47 | 48 | @Get('/:id') 49 | @OnUndefined(PetNotFoundError) 50 | @ResponseSchema(PetResponse) 51 | public one(@Param('id') id: string): Promise { 52 | return this.petService.findOne(id); 53 | } 54 | 55 | @Post() 56 | @ResponseSchema(PetResponse) 57 | public create(@Body({ required: true }) body: CreatePetBody): Promise { 58 | const pet = new Pet(); 59 | pet.age = body.age; 60 | pet.name = body.name; 61 | pet.userId = body.userId; 62 | 63 | return this.petService.create(pet); 64 | } 65 | 66 | @Put('/:id') 67 | @ResponseSchema(PetResponse) 68 | public update(@Param('id') id: string, @Body() body: BasePet): Promise { 69 | const pet = new Pet(); 70 | pet.age = body.age; 71 | pet.name = body.name; 72 | 73 | return this.petService.update(id, pet); 74 | } 75 | 76 | @Delete('/:id') 77 | public delete(@Param('id') id: string): Promise { 78 | return this.petService.delete(id); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/api/controllers/UserController.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { IsEmail, IsNotEmpty, IsUUID, ValidateNested } from 'class-validator'; 3 | import { 4 | Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, Req 5 | } from 'routing-controllers'; 6 | import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; 7 | 8 | import { UserNotFoundError } from '../errors/UserNotFoundError'; 9 | import { User } from '../models/User'; 10 | import { UserService } from '../services/UserService'; 11 | import { PetResponse } from './PetController'; 12 | 13 | class BaseUser { 14 | @IsNotEmpty() 15 | public firstName: string; 16 | 17 | @IsNotEmpty() 18 | public lastName: string; 19 | 20 | @IsEmail() 21 | @IsNotEmpty() 22 | public email: string; 23 | 24 | @IsNotEmpty() 25 | public username: string; 26 | } 27 | 28 | export class UserResponse extends BaseUser { 29 | @IsUUID() 30 | public id: string; 31 | 32 | @ValidateNested({ each: true }) 33 | @Type(() => PetResponse) 34 | public pets: PetResponse[]; 35 | } 36 | 37 | class CreateUserBody extends BaseUser { 38 | @IsNotEmpty() 39 | public password: string; 40 | } 41 | 42 | @Authorized() 43 | @JsonController('/users') 44 | @OpenAPI({ security: [{ basicAuth: [] }] }) 45 | export class UserController { 46 | 47 | constructor( 48 | private userService: UserService 49 | ) { } 50 | 51 | @Get() 52 | @ResponseSchema(UserResponse, { isArray: true }) 53 | public find(): Promise { 54 | return this.userService.find(); 55 | } 56 | 57 | @Get('/me') 58 | @ResponseSchema(UserResponse, { isArray: true }) 59 | public findMe(@Req() req: any): Promise { 60 | return req.user; 61 | } 62 | 63 | @Get('/:id') 64 | @OnUndefined(UserNotFoundError) 65 | @ResponseSchema(UserResponse) 66 | public one(@Param('id') id: string): Promise { 67 | return this.userService.findOne(id); 68 | } 69 | 70 | @Post() 71 | @ResponseSchema(UserResponse) 72 | public create(@Body() body: CreateUserBody): Promise { 73 | const user = new User(); 74 | user.email = body.email; 75 | user.firstName = body.firstName; 76 | user.lastName = body.lastName; 77 | user.password = body.password; 78 | user.username = body.username; 79 | 80 | return this.userService.create(user); 81 | } 82 | 83 | @Put('/:id') 84 | @ResponseSchema(UserResponse) 85 | public update(@Param('id') id: string, @Body() body: BaseUser): Promise { 86 | const user = new User(); 87 | user.email = body.email; 88 | user.firstName = body.firstName; 89 | user.lastName = body.lastName; 90 | user.username = body.username; 91 | 92 | return this.userService.update(id, user); 93 | } 94 | 95 | @Delete('/:id') 96 | public delete(@Param('id') id: string): Promise { 97 | return this.userService.delete(id); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/api/controllers/requests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tecch/express-typescript-boilerplate/17727010f7f098d8ca8b78da0a5b3c7297659381/src/api/controllers/requests/.gitkeep -------------------------------------------------------------------------------- /src/api/controllers/responses/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tecch/express-typescript-boilerplate/17727010f7f098d8ca8b78da0a5b3c7297659381/src/api/controllers/responses/.gitkeep -------------------------------------------------------------------------------- /src/api/errors/PetNotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from 'routing-controllers'; 2 | 3 | export class PetNotFoundError extends HttpError { 4 | constructor() { 5 | super(404, 'Pet not found!'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/errors/UserNotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from 'routing-controllers'; 2 | 3 | export class UserNotFoundError extends HttpError { 4 | constructor() { 5 | super(404, 'User not found!'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/interceptors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tecch/express-typescript-boilerplate/17727010f7f098d8ca8b78da0a5b3c7297659381/src/api/interceptors/.gitkeep -------------------------------------------------------------------------------- /src/api/middlewares/CompressionMiddleware.ts: -------------------------------------------------------------------------------- 1 | import compression from 'compression'; 2 | import * as express from 'express'; 3 | import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; 4 | 5 | @Middleware({ type: 'before' }) 6 | export class CompressionMiddleware implements ExpressMiddlewareInterface { 7 | 8 | public use(req: express.Request, res: express.Response, next: express.NextFunction): any { 9 | return compression()(req, res, next); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/api/middlewares/ErrorHandlerMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { ExpressErrorMiddlewareInterface, HttpError, Middleware } from 'routing-controllers'; 3 | 4 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 5 | import { env } from '../../env'; 6 | 7 | @Middleware({ type: 'after' }) 8 | export class ErrorHandlerMiddleware implements ExpressErrorMiddlewareInterface { 9 | 10 | public isProduction = env.isProduction; 11 | 12 | constructor( 13 | @Logger(__filename) private log: LoggerInterface 14 | ) { } 15 | 16 | public error(error: HttpError, req: express.Request, res: express.Response, next: express.NextFunction): void { 17 | res.status(error.httpCode || 500); 18 | res.json({ 19 | name: error.name, 20 | message: error.message, 21 | errors: error[`errors`] || [], 22 | }); 23 | 24 | if (this.isProduction) { 25 | this.log.error(error.name, error.message); 26 | } else { 27 | this.log.error(error.name, error.stack); 28 | } 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/api/middlewares/LogMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import morgan from 'morgan'; 3 | import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; 4 | 5 | import { env } from '../../env'; 6 | import { Logger } from '../../lib/logger'; 7 | 8 | @Middleware({ type: 'before' }) 9 | export class LogMiddleware implements ExpressMiddlewareInterface { 10 | 11 | private log = new Logger(__dirname); 12 | 13 | public use(req: express.Request, res: express.Response, next: express.NextFunction): any { 14 | return morgan(env.log.output, { 15 | stream: { 16 | write: this.log.info.bind(this.log), 17 | }, 18 | })(req, res, next); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/api/middlewares/SecurityHstsMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as helmet from 'helmet'; 3 | import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; 4 | 5 | @Middleware({ type: 'before' }) 6 | export class SecurityHstsMiddleware implements ExpressMiddlewareInterface { 7 | 8 | public use(req: express.Request, res: express.Response, next: express.NextFunction): any { 9 | return helmet.hsts({ 10 | maxAge: 31536000, 11 | includeSubdomains: true, 12 | })(req, res, next); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/api/middlewares/SecurityMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import helmet from 'helmet'; 3 | import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; 4 | 5 | @Middleware({ type: 'before' }) 6 | export class SecurityMiddleware implements ExpressMiddlewareInterface { 7 | 8 | public use(req: express.Request, res: express.Response, next: express.NextFunction): any { 9 | return helmet()(req, res, next); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/api/middlewares/SecurityNoCacheMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as helmet from 'helmet'; 3 | import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; 4 | 5 | @Middleware({ type: 'before' }) 6 | export class SecurityNoCacheMiddleware implements ExpressMiddlewareInterface { 7 | 8 | public use(req: express.Request, res: express.Response, next: express.NextFunction): any { 9 | return helmet.noCache()(req, res, next); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/api/models/Pet.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; 3 | 4 | import { User } from './User'; 5 | 6 | @Entity() 7 | export class Pet { 8 | 9 | @PrimaryColumn('uuid') 10 | public id: string; 11 | 12 | @IsNotEmpty() 13 | @Column() 14 | public name: string; 15 | 16 | @IsNotEmpty() 17 | @Column() 18 | public age: number; 19 | 20 | @Column({ 21 | name: 'user_id', 22 | nullable: true, 23 | }) 24 | public userId: string; 25 | 26 | @ManyToOne(type => User, user => user.pets) 27 | @JoinColumn({ name: 'user_id' }) 28 | public user: User; 29 | 30 | public toString(): string { 31 | return `${this.name}`; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/api/models/User.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | import { Exclude } from 'class-transformer'; 3 | import { IsNotEmpty } from 'class-validator'; 4 | import { BeforeInsert, Column, Entity, OneToMany, PrimaryColumn } from 'typeorm'; 5 | 6 | import { Pet } from './Pet'; 7 | 8 | @Entity() 9 | export class User { 10 | 11 | public static hashPassword(password: string): Promise { 12 | return new Promise((resolve, reject) => { 13 | bcrypt.hash(password, 10, (err, hash) => { 14 | if (err) { 15 | return reject(err); 16 | } 17 | resolve(hash); 18 | }); 19 | }); 20 | } 21 | 22 | public static comparePassword(user: User, password: string): Promise { 23 | return new Promise((resolve, reject) => { 24 | bcrypt.compare(password, user.password, (err, res) => { 25 | resolve(res === true); 26 | }); 27 | }); 28 | } 29 | 30 | @PrimaryColumn('uuid') 31 | public id: string; 32 | 33 | @IsNotEmpty() 34 | @Column({ name: 'first_name' }) 35 | public firstName: string; 36 | 37 | @IsNotEmpty() 38 | @Column({ name: 'last_name' }) 39 | public lastName: string; 40 | 41 | @IsNotEmpty() 42 | @Column() 43 | public email: string; 44 | 45 | @IsNotEmpty() 46 | @Column() 47 | @Exclude() 48 | public password: string; 49 | 50 | @IsNotEmpty() 51 | @Column() 52 | public username: string; 53 | 54 | @OneToMany(type => Pet, pet => pet.user) 55 | public pets: Pet[]; 56 | 57 | public toString(): string { 58 | return `${this.firstName} ${this.lastName} (${this.email})`; 59 | } 60 | 61 | @BeforeInsert() 62 | public async hashPassword(): Promise { 63 | this.password = await User.hashPassword(this.password); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/api/repositories/PetRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | 3 | import { Pet } from '../models/Pet'; 4 | 5 | @EntityRepository(Pet) 6 | export class PetRepository extends Repository { 7 | 8 | /** 9 | * Find by user_id is used for our data-loader to get all needed pets in one query. 10 | */ 11 | public findByUserIds(ids: string[]): Promise { 12 | return this.createQueryBuilder() 13 | .select() 14 | .where(`pet.user_id IN (${ids.map(id => `'${id}'`).join(', ')})`) 15 | .getMany(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/api/repositories/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | 3 | import { User } from '../models/User'; 4 | 5 | @EntityRepository(User) 6 | export class UserRepository extends Repository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/api/resolvers/PetResolver.ts: -------------------------------------------------------------------------------- 1 | import DataLoader from 'dataloader'; 2 | import { Arg, Ctx, FieldResolver, Mutation, Query, Resolver, Root } from 'type-graphql'; 3 | import { Service } from 'typedi'; 4 | 5 | import { DLoader } from '../../decorators/DLoader'; 6 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 7 | import { Context } from '../Context'; 8 | import { Pet as PetModel } from '../models/Pet'; 9 | import { User as UserModel } from '../models/User'; 10 | import { PetService } from '../services/PetService'; 11 | import { PetInput } from '../types/input/PetInput'; 12 | import { Pet } from '../types/Pet'; 13 | 14 | @Service() 15 | @Resolver(of => Pet) 16 | export class PetResolver { 17 | 18 | constructor( 19 | private petService: PetService, 20 | @Logger(__filename) private log: LoggerInterface, 21 | @DLoader(UserModel) private userLoader: DataLoader 22 | ) { } 23 | 24 | @Query(returns => [Pet]) 25 | public pets(@Ctx() { requestId }: Context): Promise { 26 | this.log.info(`{${requestId}} Find all users`); 27 | return this.petService.find(); 28 | } 29 | 30 | @Mutation(returns => Pet) 31 | public async addPet(@Arg('pet') pet: PetInput): Promise { 32 | const newPet = new PetModel(); 33 | newPet.name = pet.name; 34 | newPet.age = pet.age; 35 | return this.petService.create(newPet); 36 | } 37 | 38 | @FieldResolver() 39 | public async owner(@Root() pet: PetModel): Promise { 40 | if (pet.userId) { 41 | return this.userLoader.load(pet.userId); 42 | } 43 | // return this.userService.findOne(`${pet.userId}`); 44 | } 45 | 46 | // user: createDataLoader(UserRepository), 47 | 48 | // petsByUserIds: createDataLoader(PetRepository, { 49 | // method: 'findByUserIds', 50 | // key: 'userId', 51 | // multiple: true, 52 | // }), 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/api/resolvers/UserResolver.ts: -------------------------------------------------------------------------------- 1 | import { FieldResolver, Query, Resolver, Root } from 'type-graphql'; 2 | import { Service } from 'typedi'; 3 | 4 | import { User as UserModel } from '../models/User'; 5 | import { PetService } from '../services/PetService'; 6 | import { UserService } from '../services/UserService'; 7 | import { User } from '../types/User'; 8 | 9 | @Service() 10 | @Resolver(of => User) 11 | export class UserResolver { 12 | 13 | constructor( 14 | private userService: UserService, 15 | private petService: PetService 16 | ) {} 17 | 18 | @Query(returns => [User]) 19 | public users(): Promise { 20 | return this.userService.find(); 21 | } 22 | 23 | @FieldResolver() 24 | public async pets(@Root() user: UserModel): Promise { 25 | return this.petService.findByUser(user); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/api/services/PetService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { OrmRepository } from 'typeorm-typedi-extensions'; 3 | import uuid from 'uuid'; 4 | 5 | import { EventDispatcher, EventDispatcherInterface } from '../../decorators/EventDispatcher'; 6 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 7 | import { Pet } from '../models/Pet'; 8 | import { User } from '../models/User'; 9 | import { PetRepository } from '../repositories/PetRepository'; 10 | import { events } from '../subscribers/events'; 11 | 12 | @Service() 13 | export class PetService { 14 | 15 | constructor( 16 | @OrmRepository() private petRepository: PetRepository, 17 | @EventDispatcher() private eventDispatcher: EventDispatcherInterface, 18 | @Logger(__filename) private log: LoggerInterface 19 | ) { } 20 | 21 | public find(): Promise { 22 | this.log.info('Find all pets'); 23 | return this.petRepository.find(); 24 | } 25 | 26 | public findByUser(user: User): Promise { 27 | this.log.info('Find all pets of the user', user.toString()); 28 | return this.petRepository.find({ 29 | where: { 30 | userId: user.id, 31 | }, 32 | }); 33 | } 34 | 35 | public findOne(id: string): Promise { 36 | this.log.info('Find all pets'); 37 | return this.petRepository.findOne({ id }); 38 | } 39 | 40 | public async create(pet: Pet): Promise { 41 | this.log.info('Create a new pet => ', pet.toString()); 42 | pet.id = uuid.v1(); 43 | const newPet = await this.petRepository.save(pet); 44 | this.eventDispatcher.dispatch(events.pet.created, newPet); 45 | return newPet; 46 | } 47 | 48 | public update(id: string, pet: Pet): Promise { 49 | this.log.info('Update a pet'); 50 | pet.id = id; 51 | return this.petRepository.save(pet); 52 | } 53 | 54 | public async delete(id: string): Promise { 55 | this.log.info('Delete a pet'); 56 | await this.petRepository.delete(id); 57 | return; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/api/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { OrmRepository } from 'typeorm-typedi-extensions'; 3 | import uuid from 'uuid'; 4 | 5 | import { EventDispatcher, EventDispatcherInterface } from '../../decorators/EventDispatcher'; 6 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 7 | import { User } from '../models/User'; 8 | import { UserRepository } from '../repositories/UserRepository'; 9 | import { events } from '../subscribers/events'; 10 | 11 | @Service() 12 | export class UserService { 13 | 14 | constructor( 15 | @OrmRepository() private userRepository: UserRepository, 16 | @EventDispatcher() private eventDispatcher: EventDispatcherInterface, 17 | @Logger(__filename) private log: LoggerInterface 18 | ) { } 19 | 20 | public find(): Promise { 21 | this.log.info('Find all users'); 22 | return this.userRepository.find({ relations: ['pets'] }); 23 | } 24 | 25 | public findOne(id: string): Promise { 26 | this.log.info('Find one user'); 27 | return this.userRepository.findOne({ id }); 28 | } 29 | 30 | public async create(user: User): Promise { 31 | this.log.info('Create a new user => ', user.toString()); 32 | user.id = uuid.v1(); 33 | const newUser = await this.userRepository.save(user); 34 | this.eventDispatcher.dispatch(events.user.created, newUser); 35 | return newUser; 36 | } 37 | 38 | public update(id: string, user: User): Promise { 39 | this.log.info('Update a user'); 40 | user.id = id; 41 | return this.userRepository.save(user); 42 | } 43 | 44 | public async delete(id: string): Promise { 45 | this.log.info('Delete a user'); 46 | await this.userRepository.delete(id); 47 | return; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/api/subscribers/UserEventSubscriber.ts: -------------------------------------------------------------------------------- 1 | import { EventSubscriber, On } from 'event-dispatch'; 2 | 3 | import { Logger } from '../../lib/logger'; 4 | import { User } from '../models/User'; 5 | import { events } from './events'; 6 | 7 | const log = new Logger(__filename); 8 | 9 | @EventSubscriber() 10 | export class UserEventSubscriber { 11 | 12 | @On(events.user.created) 13 | public onUserCreate(user: User): void { 14 | log.info('User ' + user.toString() + ' created!'); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/api/subscribers/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * events 3 | * --------------------- 4 | * Define all your possible custom events here. 5 | */ 6 | export const events = { 7 | user: { 8 | created: 'onUserCreate', 9 | }, 10 | pet: { 11 | created: 'onPetCreate', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/types/Pet.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, Int, ObjectType } from 'type-graphql'; 2 | 3 | import { User } from './User'; 4 | 5 | @ObjectType({ 6 | description: 'Pet object.', 7 | }) 8 | export class Pet { 9 | 10 | @Field(type => ID) 11 | public id: string; 12 | 13 | @Field({ 14 | description: 'The name of the pet.', 15 | }) 16 | public name: string; 17 | 18 | @Field(type => Int, { 19 | description: 'The age of the pet in years.', 20 | }) 21 | public age: number; 22 | 23 | @Field(type => User, { 24 | nullable: true, 25 | }) 26 | public owner: User; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/api/types/User.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from 'type-graphql'; 2 | 3 | import { Pet } from './Pet'; 4 | 5 | @ObjectType({ 6 | description: 'User object.', 7 | }) 8 | export class User { 9 | 10 | @Field(type => ID) 11 | public id: string; 12 | 13 | @Field({ 14 | description: 'The first name of the user.', 15 | }) 16 | public firstName: string; 17 | 18 | @Field({ 19 | description: 'The last name of the user.', 20 | }) 21 | public lastName: string; 22 | 23 | @Field({ 24 | description: 'The email of the user.', 25 | }) 26 | public email: string; 27 | 28 | @Field(type => [Pet], { 29 | description: 'A list of pets which belong to the user.', 30 | }) 31 | public pets: Pet[]; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/api/types/input/PetInput.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType, Int } from 'type-graphql'; 2 | 3 | import { Pet } from '../Pet'; 4 | 5 | @InputType() 6 | export class PetInput implements Partial { 7 | 8 | @Field() 9 | public name: string; 10 | 11 | @Field(type => Int, { 12 | description: 'The age of the pet in years.', 13 | }) 14 | public age: number; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/api/validators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tecch/express-typescript-boilerplate/17727010f7f098d8ca8b78da0a5b3c7297659381/src/api/validators/.gitkeep -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { bootstrapMicroframework } from 'microframework-w3tec'; 4 | 5 | import { banner } from './lib/banner'; 6 | import { Logger } from './lib/logger'; 7 | import { eventDispatchLoader } from './loaders/eventDispatchLoader'; 8 | import { expressLoader } from './loaders/expressLoader'; 9 | import { graphqlLoader } from './loaders/graphqlLoader'; 10 | import { homeLoader } from './loaders/homeLoader'; 11 | import { iocLoader } from './loaders/iocLoader'; 12 | import { monitorLoader } from './loaders/monitorLoader'; 13 | import { publicLoader } from './loaders/publicLoader'; 14 | import { swaggerLoader } from './loaders/swaggerLoader'; 15 | import { typeormLoader } from './loaders/typeormLoader'; 16 | import { winstonLoader } from './loaders/winstonLoader'; 17 | 18 | /** 19 | * EXPRESS TYPESCRIPT BOILERPLATE 20 | * ---------------------------------------- 21 | * 22 | * This is a boilerplate for Node.js Application written in TypeScript. 23 | * The basic layer of this app is express. For further information visit 24 | * the 'README.md' file. 25 | */ 26 | const log = new Logger(__filename); 27 | 28 | bootstrapMicroframework({ 29 | /** 30 | * Loader is a place where you can configure all your modules during microframework 31 | * bootstrap process. All loaders are executed one by one in a sequential order. 32 | */ 33 | loaders: [ 34 | winstonLoader, 35 | iocLoader, 36 | eventDispatchLoader, 37 | typeormLoader, 38 | expressLoader, 39 | swaggerLoader, 40 | monitorLoader, 41 | homeLoader, 42 | publicLoader, 43 | graphqlLoader, 44 | ], 45 | }) 46 | .then(() => banner(log)) 47 | .catch(error => log.error('Application is crashed: ' + error)); 48 | -------------------------------------------------------------------------------- /src/auth/AuthService.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Service } from 'typedi'; 3 | import { OrmRepository } from 'typeorm-typedi-extensions'; 4 | 5 | import { User } from '../api/models/User'; 6 | import { UserRepository } from '../api/repositories/UserRepository'; 7 | import { Logger, LoggerInterface } from '../decorators/Logger'; 8 | 9 | @Service() 10 | export class AuthService { 11 | 12 | constructor( 13 | @Logger(__filename) private log: LoggerInterface, 14 | @OrmRepository() private userRepository: UserRepository 15 | ) { } 16 | 17 | public parseBasicAuthFromRequest(req: express.Request): { username: string, password: string } { 18 | const authorization = req.header('authorization'); 19 | 20 | if (authorization && authorization.split(' ')[0] === 'Basic') { 21 | this.log.info('Credentials provided by the client'); 22 | const decodedBase64 = Buffer.from(authorization.split(' ')[1], 'base64').toString('ascii'); 23 | const username = decodedBase64.split(':')[0]; 24 | const password = decodedBase64.split(':')[1]; 25 | if (username && password) { 26 | return { username, password }; 27 | } 28 | } 29 | 30 | this.log.info('No credentials provided by the client'); 31 | return undefined; 32 | } 33 | 34 | public async validateUser(username: string, password: string): Promise { 35 | const user = await this.userRepository.findOne({ 36 | where: { 37 | username, 38 | }, 39 | }); 40 | 41 | if (await User.comparePassword(user, password)) { 42 | return user; 43 | } 44 | 45 | return undefined; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/auth/authorizationChecker.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'routing-controllers'; 2 | import { Container } from 'typedi'; 3 | import { Connection } from 'typeorm'; 4 | 5 | import { Logger } from '../lib/logger'; 6 | import { AuthService } from './AuthService'; 7 | 8 | export function authorizationChecker(connection: Connection): (action: Action, roles: any[]) => Promise | boolean { 9 | const log = new Logger(__filename); 10 | const authService = Container.get(AuthService); 11 | 12 | return async function innerAuthorizationChecker(action: Action, roles: string[]): Promise { 13 | // here you can use request/response objects from action 14 | // also if decorator defines roles it needs to access the action 15 | // you can use them to provide granular access check 16 | // checker must return either boolean (true or false) 17 | // either promise that resolves a boolean value 18 | const credentials = authService.parseBasicAuthFromRequest(action.request); 19 | 20 | if (credentials === undefined) { 21 | log.warn('No credentials given'); 22 | return false; 23 | } 24 | 25 | action.request.user = await authService.validateUser(credentials.username, credentials.password); 26 | if (action.request.user === undefined) { 27 | log.warn('Invalid credentials given'); 28 | return false; 29 | } 30 | 31 | log.info('Successfully checked credentials'); 32 | return true; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/auth/currentUserChecker.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'routing-controllers'; 2 | import { Connection } from 'typeorm'; 3 | 4 | import { User } from '../api/models/User'; 5 | 6 | export function currentUserChecker(connection: Connection): (action: Action) => Promise { 7 | return async function innerCurrentUserChecker(action: Action): Promise { 8 | return action.request.user; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/database/factories/PetFactory.ts: -------------------------------------------------------------------------------- 1 | import * as Faker from 'faker'; 2 | import { define } from 'typeorm-seeding'; 3 | import * as uuid from 'uuid'; 4 | 5 | import { Pet } from '../../../src/api/models/Pet'; 6 | 7 | define(Pet, (faker: typeof Faker) => { 8 | const gender = faker.random.number(1); 9 | const name = faker.name.firstName(gender); 10 | 11 | const pet = new Pet(); 12 | pet.id = uuid.v1(); 13 | pet.name = name; 14 | pet.age = faker.random.number(); 15 | return pet; 16 | }); 17 | -------------------------------------------------------------------------------- /src/database/factories/UserFactory.ts: -------------------------------------------------------------------------------- 1 | import * as Faker from 'faker'; 2 | import { define } from 'typeorm-seeding'; 3 | import * as uuid from 'uuid'; 4 | 5 | import { User } from '../../../src/api/models/User'; 6 | 7 | define(User, (faker: typeof Faker, settings: { role: string }) => { 8 | const gender = faker.random.number(1); 9 | const firstName = faker.name.firstName(gender); 10 | const lastName = faker.name.lastName(gender); 11 | const email = faker.internet.email(firstName, lastName); 12 | const username = faker.internet.userName(firstName, lastName); 13 | 14 | const user = new User(); 15 | user.id = uuid.v1(); 16 | user.firstName = firstName; 17 | user.lastName = lastName; 18 | user.email = email; 19 | user.username = username; 20 | user.password = '1234'; 21 | return user; 22 | }); 23 | -------------------------------------------------------------------------------- /src/database/migrations/1511105183653-CreateUserTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | 3 | export class CreateUserTable1511105183653 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | const table = new Table({ 7 | name: 'user', 8 | columns: [ 9 | { 10 | name: 'id', 11 | type: 'varchar', 12 | length: '255', 13 | isPrimary: true, 14 | isNullable: false, 15 | }, { 16 | name: 'first_name', 17 | type: 'varchar', 18 | length: '255', 19 | isPrimary: false, 20 | isNullable: false, 21 | }, { 22 | name: 'last_name', 23 | type: 'varchar', 24 | length: '255', 25 | isPrimary: false, 26 | isNullable: false, 27 | }, { 28 | name: 'email', 29 | type: 'varchar', 30 | length: '255', 31 | isPrimary: false, 32 | isNullable: false, 33 | }, { 34 | name: 'username', 35 | type: 'varchar', 36 | length: '255', 37 | isPrimary: false, 38 | isNullable: false, 39 | } , { 40 | name: 'password', 41 | type: 'varchar', 42 | length: '255', 43 | isPrimary: false, 44 | isNullable: false, 45 | }, 46 | ], 47 | }); 48 | await queryRunner.createTable(table); 49 | } 50 | 51 | public async down(queryRunner: QueryRunner): Promise { 52 | await queryRunner.dropTable('user'); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/database/migrations/1512663524808-CreatePetTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | 3 | export class CreatePetTable1512663524808 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | const table = new Table({ 7 | name: 'pet', 8 | columns: [ 9 | { 10 | name: 'id', 11 | type: 'varchar', 12 | length: '255', 13 | isPrimary: true, 14 | isNullable: false, 15 | }, { 16 | name: 'name', 17 | type: 'varchar', 18 | length: '255', 19 | isPrimary: false, 20 | isNullable: false, 21 | }, { 22 | name: 'age', 23 | type: 'int', 24 | isPrimary: false, 25 | isNullable: false, 26 | }, { 27 | name: 'user_id', 28 | type: 'varchar', 29 | length: '255', 30 | isPrimary: false, 31 | isNullable: true, 32 | }, 33 | ], 34 | }); 35 | await queryRunner.createTable(table); 36 | } 37 | 38 | public async down(queryRunner: QueryRunner): Promise { 39 | await queryRunner.dropTable('pet'); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/database/migrations/1512663990063-AddUserRelationToPetTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, TableForeignKey } from 'typeorm'; 2 | 3 | export class AddUserRelationToPetTable1512663990063 implements MigrationInterface { 4 | 5 | private tableForeignKey = new TableForeignKey({ 6 | name: 'fk_user_pet', 7 | columnNames: ['user_id'], 8 | referencedColumnNames: ['id'], 9 | referencedTableName: 'user', 10 | onDelete: 'CASCADE', 11 | }); 12 | 13 | public async up(queryRunner: QueryRunner): Promise { 14 | await queryRunner.createForeignKey('pet', this.tableForeignKey); 15 | } 16 | 17 | public async down(queryRunner: QueryRunner): Promise { 18 | await queryRunner.dropForeignKey('pet', this.tableForeignKey); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/database/seeds/CreateBruce.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'typeorm'; 2 | import { Factory, Seed } from 'typeorm-seeding'; 3 | import * as uuid from 'uuid'; 4 | 5 | import { User } from '../../../src/api/models/User'; 6 | 7 | export class CreateBruce implements Seed { 8 | 9 | public async seed(factory: Factory, connection: Connection): Promise { 10 | // const userFactory = factory(User as any); 11 | // const adminUserFactory = userFactory({ role: 'admin' }); 12 | 13 | // const bruce = await adminUserFactory.make(); 14 | // console.log(bruce); 15 | 16 | // const bruce2 = await adminUserFactory.seed(); 17 | // console.log(bruce2); 18 | 19 | // const bruce3 = await adminUserFactory 20 | // .map(async (e: User) => { 21 | // e.firstName = 'Bruce'; 22 | // return e; 23 | // }) 24 | // .seed(); 25 | // console.log(bruce3); 26 | 27 | // return bruce; 28 | 29 | // const connection = await factory.getConnection(); 30 | const em = connection.createEntityManager(); 31 | 32 | const user = new User(); 33 | user.id = uuid.v1(); 34 | user.firstName = 'Bruce'; 35 | user.lastName = 'Wayne'; 36 | user.email = 'bruce.wayne@wayne-enterprises.com'; 37 | user.username = 'bruce'; 38 | user.password = '1234'; 39 | return await em.save(user); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/database/seeds/CreatePets.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'typeorm'; 2 | import { Factory, Seed, times } from 'typeorm-seeding'; 3 | 4 | import { Pet } from '../../../src/api/models/Pet'; 5 | import { User } from '../../../src/api/models/User'; 6 | 7 | export class CreatePets implements Seed { 8 | 9 | public async seed(factory: Factory, connection: Connection): Promise { 10 | const em = connection.createEntityManager(); 11 | await times(10, async (n) => { 12 | const pet = await factory(Pet)().seed(); 13 | const user = await factory(User)().make(); 14 | user.pets = [pet]; 15 | return await em.save(user); 16 | }); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/database/seeds/CreateUsers.ts: -------------------------------------------------------------------------------- 1 | import { Factory, Seed } from 'typeorm-seeding'; 2 | import { Connection } from 'typeorm/connection/Connection'; 3 | 4 | import { User } from '../../../src/api/models/User'; 5 | 6 | export class CreateUsers implements Seed { 7 | 8 | public async seed(factory: Factory, connection: Connection): Promise { 9 | await factory(User)().seedMany(10); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/decorators/DLoader.ts: -------------------------------------------------------------------------------- 1 | import { Container, ObjectType } from 'typedi'; 2 | 3 | import { createDataLoader, CreateDataLoaderOptions } from '../lib/graphql'; 4 | 5 | export function DLoader(obj: ObjectType, options: CreateDataLoaderOptions = {}): ParameterDecorator { 6 | return (object, propertyKey, index) => { 7 | const dataLoader = createDataLoader(obj, options); 8 | const propertyName = propertyKey ? propertyKey.toString() : ''; 9 | Container.registerHandler({ object, propertyName, index, value: () => dataLoader }); 10 | }; 11 | } 12 | 13 | export * from '../lib/graphql'; 14 | -------------------------------------------------------------------------------- /src/decorators/EventDispatcher.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher as EventDispatcherClass } from 'event-dispatch'; 2 | import { Container } from 'typedi'; 3 | 4 | export function EventDispatcher(): any { 5 | return (object: any, propertyName: string, index?: number): any => { 6 | const eventDispatcher = new EventDispatcherClass(); 7 | Container.registerHandler({ object, propertyName, index, value: () => eventDispatcher }); 8 | }; 9 | } 10 | 11 | export { EventDispatcher as EventDispatcherInterface } from 'event-dispatch'; 12 | -------------------------------------------------------------------------------- /src/decorators/Logger.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'typedi'; 2 | 3 | import { Logger as WinstonLogger } from '../lib/logger'; 4 | 5 | export function Logger(scope: string): ParameterDecorator { 6 | return (object, propertyKey, index): any => { 7 | const logger = new WinstonLogger(scope); 8 | const propertyName = propertyKey ? propertyKey.toString() : ''; 9 | Container.registerHandler({ object, propertyName, index, value: () => logger }); 10 | }; 11 | } 12 | 13 | export { LoggerInterface } from '../lib/logger'; 14 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as path from 'path'; 3 | 4 | import * as pkg from '../package.json'; 5 | import { 6 | getOsEnv, getOsEnvOptional, getOsPath, getOsPaths, normalizePort, toBool, toNumber 7 | } from './lib/env'; 8 | 9 | /** 10 | * Load .env file or for tests the .env.test file. 11 | */ 12 | dotenv.config({ path: path.join(process.cwd(), `.env${((process.env.NODE_ENV === 'test') ? '.test' : '')}`) }); 13 | 14 | /** 15 | * Environment variables 16 | */ 17 | export const env = { 18 | node: process.env.NODE_ENV || 'development', 19 | isProduction: process.env.NODE_ENV === 'production', 20 | isTest: process.env.NODE_ENV === 'test', 21 | isDevelopment: process.env.NODE_ENV === 'development', 22 | app: { 23 | name: getOsEnv('APP_NAME'), 24 | version: (pkg as any).version, 25 | description: (pkg as any).description, 26 | host: getOsEnv('APP_HOST'), 27 | schema: getOsEnv('APP_SCHEMA'), 28 | routePrefix: getOsEnv('APP_ROUTE_PREFIX'), 29 | port: normalizePort(process.env.PORT || getOsEnv('APP_PORT')), 30 | banner: toBool(getOsEnv('APP_BANNER')), 31 | dirs: { 32 | migrations: getOsPaths('TYPEORM_MIGRATIONS'), 33 | migrationsDir: getOsPath('TYPEORM_MIGRATIONS_DIR'), 34 | entities: getOsPaths('TYPEORM_ENTITIES'), 35 | entitiesDir: getOsPath('TYPEORM_ENTITIES_DIR'), 36 | controllers: getOsPaths('CONTROLLERS'), 37 | middlewares: getOsPaths('MIDDLEWARES'), 38 | interceptors: getOsPaths('INTERCEPTORS'), 39 | subscribers: getOsPaths('SUBSCRIBERS'), 40 | resolvers: getOsPaths('RESOLVERS'), 41 | }, 42 | }, 43 | log: { 44 | level: getOsEnv('LOG_LEVEL'), 45 | json: toBool(getOsEnvOptional('LOG_JSON')), 46 | output: getOsEnv('LOG_OUTPUT'), 47 | }, 48 | db: { 49 | type: getOsEnv('TYPEORM_CONNECTION'), 50 | host: getOsEnvOptional('TYPEORM_HOST'), 51 | port: toNumber(getOsEnvOptional('TYPEORM_PORT')), 52 | username: getOsEnvOptional('TYPEORM_USERNAME'), 53 | password: getOsEnvOptional('TYPEORM_PASSWORD'), 54 | database: getOsEnv('TYPEORM_DATABASE'), 55 | synchronize: toBool(getOsEnvOptional('TYPEORM_SYNCHRONIZE')), 56 | logging: getOsEnv('TYPEORM_LOGGING'), 57 | }, 58 | graphql: { 59 | enabled: toBool(getOsEnv('GRAPHQL_ENABLED')), 60 | route: getOsEnv('GRAPHQL_ROUTE'), 61 | editor: toBool(getOsEnv('GRAPHQL_EDITOR')), 62 | }, 63 | swagger: { 64 | enabled: toBool(getOsEnv('SWAGGER_ENABLED')), 65 | route: getOsEnv('SWAGGER_ROUTE'), 66 | username: getOsEnv('SWAGGER_USERNAME'), 67 | password: getOsEnv('SWAGGER_PASSWORD'), 68 | }, 69 | monitor: { 70 | enabled: toBool(getOsEnv('MONITOR_ENABLED')), 71 | route: getOsEnv('MONITOR_ROUTE'), 72 | username: getOsEnv('MONITOR_USERNAME'), 73 | password: getOsEnv('MONITOR_PASSWORD'), 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/lib/banner.ts: -------------------------------------------------------------------------------- 1 | import { env } from '../env'; 2 | import { Logger } from '../lib/logger'; 3 | 4 | export function banner(log: Logger): void { 5 | if (env.app.banner) { 6 | const route = () => `${env.app.schema}://${env.app.host}:${env.app.port}`; 7 | log.info(``); 8 | log.info(`Aloha, your app is ready on ${route()}${env.app.routePrefix}`); 9 | log.info(`To shut it down, press + C at any time.`); 10 | log.info(``); 11 | log.info('-------------------------------------------------------'); 12 | log.info(`Environment : ${env.node}`); 13 | log.info(`Version : ${env.app.version}`); 14 | log.info(``); 15 | log.info(`API Info : ${route()}${env.app.routePrefix}`); 16 | if (env.graphql.enabled) { 17 | log.info(`GraphQL : ${route()}${env.graphql.route}`); 18 | } 19 | if (env.swagger.enabled) { 20 | log.info(`Swagger : ${route()}${env.swagger.route}`); 21 | } 22 | if (env.monitor.enabled) { 23 | log.info(`Monitor : ${route()}${env.monitor.route}`); 24 | } 25 | log.info('-------------------------------------------------------'); 26 | log.info(''); 27 | } else { 28 | log.info(`Application is up and running.`); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/env/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | -------------------------------------------------------------------------------- /src/lib/env/utils.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export function getOsEnv(key: string): string { 4 | if (typeof process.env[key] === 'undefined') { 5 | throw new Error(`Environment variable ${key} is not set.`); 6 | } 7 | 8 | return process.env[key] as string; 9 | } 10 | 11 | export function getOsEnvOptional(key: string): string | undefined { 12 | return process.env[key]; 13 | } 14 | 15 | export function getPath(path: string): string { 16 | return (process.env.NODE_ENV === 'production') 17 | ? join(process.cwd(), path.replace('src/', 'dist/').slice(0, -3) + '.js') 18 | : join(process.cwd(), path); 19 | } 20 | 21 | export function getPaths(paths: string[]): string[] { 22 | return paths.map(p => getPath(p)); 23 | } 24 | 25 | export function getOsPath(key: string): string { 26 | return getPath(getOsEnv(key)); 27 | } 28 | 29 | export function getOsPaths(key: string): string[] { 30 | return getPaths(getOsEnvArray(key)); 31 | } 32 | 33 | export function getOsEnvArray(key: string, delimiter: string = ','): string[] { 34 | return process.env[key] && process.env[key].split(delimiter) || []; 35 | } 36 | 37 | export function toNumber(value: string): number { 38 | return parseInt(value, 10); 39 | } 40 | 41 | export function toBool(value: string): boolean { 42 | return value === 'true'; 43 | } 44 | 45 | export function normalizePort(port: string): number | string | boolean { 46 | const parsedPort = parseInt(port, 10); 47 | if (isNaN(parsedPort)) { // named pipe 48 | return port; 49 | } 50 | if (parsedPort >= 0) { // port number 51 | return parsedPort; 52 | } 53 | return false; 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/graphql/graphql-error-handling.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLSchema } from 'graphql'; 2 | import * as uuid from 'uuid'; 3 | 4 | import { env } from '../../env'; 5 | import { Logger } from '../../lib/logger'; 6 | 7 | // This feature is a copy from https://github.com/kadirahq/graphql-errors 8 | const logger = new Logger('app:errors'); 9 | 10 | // Mark field/type/schema 11 | export const Processed = Symbol(); 12 | 13 | // Used to identify UserErrors 14 | export const IsUserError = Symbol(); 15 | 16 | // UserErrors will be sent to the user 17 | export class UserError extends Error { 18 | constructor(...args: any[]) { 19 | super(args[0]); 20 | this.name = 'Error'; 21 | this.message = args[0]; 22 | this[IsUserError] = true; 23 | Error.captureStackTrace(this); 24 | } 25 | } 26 | 27 | // Modifies errors before sending to the user 28 | export let defaultHandler = (err?) => { 29 | if (err[IsUserError]) { 30 | return err; 31 | } 32 | const errId = uuid.v4(); 33 | err.message = `${err.message}: ${errId}`; 34 | if (!env.isTest) { 35 | console.error(err && err.stack || err); 36 | } 37 | if (env.isProduction) { 38 | logger.error(err); 39 | } 40 | err.message = `500: Internal Error: ${errId}`; 41 | return err; 42 | }; 43 | 44 | const maskField = (field, fn) => { 45 | const resolveFn = field.resolve; 46 | if (field[Processed] || !resolveFn) { 47 | return; 48 | } 49 | 50 | field[Processed] = true; 51 | field.resolve = async (...args) => { 52 | try { 53 | const out = resolveFn.call(undefined, ...args); 54 | return await Promise.resolve(out); 55 | } catch (e) { 56 | throw fn(e); 57 | } 58 | }; 59 | 60 | // save the original resolve function 61 | field.resolve._resolveFn = resolveFn; 62 | }; 63 | 64 | const maskType = (type, fn) => { 65 | if (type[Processed] || !type.getFields) { 66 | return; 67 | } 68 | 69 | const fields = type.getFields(); 70 | for (const fieldName in fields) { 71 | if (!Object.hasOwnProperty.call(fields, fieldName)) { 72 | continue; 73 | } 74 | maskField(fields[fieldName], fn); 75 | } 76 | }; 77 | 78 | const maskSchema = (schema, fn) => { 79 | const types = schema.getTypeMap(); 80 | for (const typeName in types) { 81 | if (!Object.hasOwnProperty.call(types, typeName)) { 82 | continue; 83 | } 84 | maskType(types[typeName], fn); 85 | } 86 | }; 87 | 88 | // Changes the default error handler function 89 | export const setDefaultHandler = (handlerFn) => { 90 | defaultHandler = handlerFn; 91 | }; 92 | 93 | // Masks graphql schemas, types or individual fields 94 | export const handlingErrors = (thing, fn = defaultHandler) => { 95 | if (thing instanceof GraphQLSchema) { 96 | maskSchema(thing, fn); 97 | } else if (thing instanceof GraphQLObjectType) { 98 | maskType(thing, fn); 99 | } else { 100 | maskField(thing, fn); 101 | } 102 | }; 103 | 104 | export const getErrorCode = (message: string): string => { 105 | if (hasErrorCode(message)) { 106 | return message.substring(0, 3); 107 | } 108 | return '500'; // unkown error code 109 | }; 110 | 111 | export const getErrorMessage = (message: string): string => { 112 | if (hasErrorCode(message)) { 113 | return message.substring(5); 114 | } 115 | return message; 116 | }; 117 | 118 | export const hasErrorCode = (error: any): boolean => { 119 | let message = error; 120 | if (error.message) { 121 | message = error.message; 122 | } 123 | const reg = new RegExp('^[0-9]{3}: '); 124 | return reg.test(message); 125 | }; 126 | -------------------------------------------------------------------------------- /src/lib/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import DataLoader from 'dataloader'; 2 | import { ObjectType } from 'typedi'; 3 | import { getCustomRepository, getRepository, Repository } from 'typeorm'; 4 | 5 | // ------------------------------------------------------------------------- 6 | // Main exports 7 | // ------------------------------------------------------------------------- 8 | 9 | export * from './graphql-error-handling'; 10 | 11 | // ------------------------------------------------------------------------- 12 | // Main Functions 13 | // ------------------------------------------------------------------------- 14 | 15 | export interface CreateDataLoaderOptions { 16 | method?: string; 17 | key?: string; 18 | multiple?: boolean; 19 | } 20 | 21 | /** 22 | * Creates a new dataloader with the typorm repository 23 | */ 24 | export function createDataLoader(obj: ObjectType, options: CreateDataLoaderOptions = {}): DataLoader { 25 | let repository; 26 | try { 27 | repository = getCustomRepository>(obj); 28 | } catch (errorRepo) { 29 | try { 30 | repository = getRepository(obj); 31 | } catch (errorModel) { 32 | throw new Error('Could not create a dataloader, because obj is nether model or repository!'); 33 | } 34 | } 35 | 36 | return new DataLoader(async (ids: number[]) => { 37 | let items = []; 38 | if (options.method) { 39 | items = await repository[options.method](ids); 40 | } else { 41 | items = await repository.findByIds(ids); 42 | } 43 | 44 | const handleBatch = (arr: any[]) => options.multiple === true ? arr : arr[0]; 45 | return ids.map(id => handleBatch(items.filter(item => item[options.key || 'id'] === id))); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/logger/Logger.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as winston from 'winston'; 3 | 4 | /** 5 | * core.Log 6 | * ------------------------------------------------ 7 | * 8 | * This is the main Logger Object. You can create a scope logger 9 | * or directly use the static log methods. 10 | * 11 | * By Default it uses the debug-adapter, but you are able to change 12 | * this in the start up process in the core/index.ts file. 13 | */ 14 | 15 | export class Logger { 16 | 17 | public static DEFAULT_SCOPE = 'app'; 18 | 19 | private static parsePathToScope(filepath: string): string { 20 | if (filepath.indexOf(path.sep) >= 0) { 21 | filepath = filepath.replace(process.cwd(), ''); 22 | filepath = filepath.replace(`${path.sep}src${path.sep}`, ''); 23 | filepath = filepath.replace(`${path.sep}dist${path.sep}`, ''); 24 | filepath = filepath.replace('.ts', ''); 25 | filepath = filepath.replace('.js', ''); 26 | filepath = filepath.replace(path.sep, ':'); 27 | } 28 | return filepath; 29 | } 30 | 31 | private scope: string; 32 | 33 | constructor(scope?: string) { 34 | this.scope = Logger.parsePathToScope((scope) ? scope : Logger.DEFAULT_SCOPE); 35 | } 36 | 37 | public debug(message: string, ...args: any[]): void { 38 | this.log('debug', message, args); 39 | } 40 | 41 | public info(message: string, ...args: any[]): void { 42 | this.log('info', message, args); 43 | } 44 | 45 | public warn(message: string, ...args: any[]): void { 46 | this.log('warn', message, args); 47 | } 48 | 49 | public error(message: string, ...args: any[]): void { 50 | this.log('error', message, args); 51 | } 52 | 53 | private log(level: string, message: string, args: any[]): void { 54 | if (winston) { 55 | winston[level](`${this.formatScope()} ${message}`, args); 56 | } 57 | } 58 | 59 | private formatScope(): string { 60 | return `[${this.scope}]`; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/logger/LoggerInterface.ts: -------------------------------------------------------------------------------- 1 | export interface LoggerInterface { 2 | debug(message: string, ...args: any[]): void; 3 | info(message: string, ...args: any[]): void; 4 | warn(message: string, ...args: any[]): void; 5 | error(message: string, ...args: any[]): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Logger'; 2 | export * from './LoggerInterface'; 3 | -------------------------------------------------------------------------------- /src/loaders/eventDispatchLoader.ts: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 3 | 4 | import { env } from '../env'; 5 | 6 | /** 7 | * eventDispatchLoader 8 | * ------------------------------ 9 | * This loads all the created subscribers into the project, so we do not have to 10 | * import them manually 11 | */ 12 | export const eventDispatchLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 13 | if (settings) { 14 | const patterns = env.app.dirs.subscribers; 15 | patterns.forEach((pattern) => { 16 | glob(pattern, (err: any, files: string[]) => { 17 | for (const file of files) { 18 | require(file); 19 | } 20 | }); 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/loaders/expressLoader.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'express'; 2 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 3 | import { createExpressServer } from 'routing-controllers'; 4 | 5 | import { authorizationChecker } from '../auth/authorizationChecker'; 6 | import { currentUserChecker } from '../auth/currentUserChecker'; 7 | import { env } from '../env'; 8 | 9 | export const expressLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 10 | if (settings) { 11 | const connection = settings.getData('connection'); 12 | 13 | /** 14 | * We create a new express server instance. 15 | * We could have also use useExpressServer here to attach controllers to an existing express instance. 16 | */ 17 | const expressApp: Application = createExpressServer({ 18 | cors: true, 19 | classTransformer: true, 20 | routePrefix: env.app.routePrefix, 21 | defaultErrorHandler: false, 22 | /** 23 | * We can add options about how routing-controllers should configure itself. 24 | * Here we specify what controllers should be registered in our express server. 25 | */ 26 | controllers: env.app.dirs.controllers, 27 | middlewares: env.app.dirs.middlewares, 28 | interceptors: env.app.dirs.interceptors, 29 | 30 | /** 31 | * Authorization features 32 | */ 33 | authorizationChecker: authorizationChecker(connection), 34 | currentUserChecker: currentUserChecker(connection), 35 | }); 36 | 37 | // Run application to listen on given port 38 | if (!env.isTest) { 39 | const server = expressApp.listen(env.app.port); 40 | settings.setData('express_server', server); 41 | } 42 | 43 | // Here we can set the data for other loaders 44 | settings.setData('express_app', expressApp); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/loaders/graphqlLoader.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import GraphQLHTTP from 'express-graphql'; 3 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 4 | import * as path from 'path'; 5 | import { buildSchema } from 'type-graphql'; 6 | import Container from 'typedi'; 7 | 8 | import { env } from '../env'; 9 | import { getErrorCode, getErrorMessage, handlingErrors } from '../lib/graphql'; 10 | 11 | export const graphqlLoader: MicroframeworkLoader = async (settings: MicroframeworkSettings | undefined) => { 12 | if (settings && env.graphql.enabled) { 13 | const expressApp = settings.getData('express_app'); 14 | 15 | const schema = await buildSchema({ 16 | resolvers: env.app.dirs.resolvers, 17 | // automatically create `schema.gql` file with schema definition in current folder 18 | emitSchemaFile: path.resolve(__dirname, '../api', 'schema.gql'), 19 | }); 20 | 21 | handlingErrors(schema); 22 | 23 | // Add graphql layer to the express app 24 | expressApp.use(env.graphql.route, (request: express.Request, response: express.Response) => { 25 | 26 | // Build GraphQLContext 27 | const requestId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); // uuid-like 28 | const container = Container.of(requestId); // get scoped container 29 | const context = { requestId, container, request, response }; // create our context 30 | container.set('context', context); // place context or other data in container 31 | 32 | // Setup GraphQL Server 33 | GraphQLHTTP({ 34 | schema, 35 | context, 36 | graphiql: env.graphql.editor, 37 | formatError: error => ({ 38 | code: getErrorCode(error.message), 39 | message: getErrorMessage(error.message), 40 | path: error.path, 41 | }), 42 | })(request, response); 43 | }); 44 | 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/loaders/homeLoader.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 3 | 4 | import { env } from '../env'; 5 | 6 | export const homeLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 7 | if (settings) { 8 | const expressApp = settings.getData('express_app'); 9 | expressApp.get( 10 | env.app.routePrefix, 11 | (req: express.Request, res: express.Response) => { 12 | return res.json({ 13 | name: env.app.name, 14 | version: env.app.version, 15 | description: env.app.description, 16 | }); 17 | } 18 | ); 19 | 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/loaders/iocLoader.ts: -------------------------------------------------------------------------------- 1 | import { useContainer as classValidatorUseContainer } from 'class-validator'; 2 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 3 | import { useContainer as routingUseContainer } from 'routing-controllers'; 4 | import { useContainer as typeGraphQLUseContainer } from 'type-graphql'; 5 | import { Container } from 'typedi'; 6 | import { useContainer as ormUseContainer } from 'typeorm'; 7 | 8 | export const iocLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 9 | 10 | /** 11 | * Setup routing-controllers to use typedi container. 12 | */ 13 | routingUseContainer(Container); 14 | ormUseContainer(Container); 15 | classValidatorUseContainer(Container); 16 | typeGraphQLUseContainer(Container); 17 | }; 18 | -------------------------------------------------------------------------------- /src/loaders/monitorLoader.ts: -------------------------------------------------------------------------------- 1 | import basicAuth from 'express-basic-auth'; 2 | import monitor from 'express-status-monitor'; 3 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 4 | 5 | import { env } from '../env'; 6 | 7 | export const monitorLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 8 | if (settings && env.monitor.enabled) { 9 | const expressApp = settings.getData('express_app'); 10 | 11 | expressApp.use(monitor()); 12 | expressApp.get( 13 | env.monitor.route, 14 | env.monitor.username ? basicAuth({ 15 | users: { 16 | [`${env.monitor.username}`]: env.monitor.password, 17 | }, 18 | challenge: true, 19 | }) : (req, res, next) => next(), 20 | monitor().pageRoute 21 | ); 22 | 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/loaders/publicLoader.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 3 | import * as path from 'path'; 4 | import favicon from 'serve-favicon'; 5 | 6 | export const publicLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 7 | if (settings) { 8 | const expressApp = settings.getData('express_app'); 9 | expressApp 10 | // Serve static files like images from the public folder 11 | .use(express.static(path.join(__dirname, '..', 'public'), { maxAge: 31557600000 })) 12 | 13 | // A favicon is a visual cue that client software, like browsers, use to identify a site 14 | .use(favicon(path.join(__dirname, '..', 'public', 'favicon.ico'))); 15 | 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/loaders/swaggerLoader.ts: -------------------------------------------------------------------------------- 1 | import { defaultMetadataStorage as classTransformerMetadataStorage } from 'class-transformer/storage'; 2 | import { getFromContainer, MetadataStorage } from 'class-validator'; 3 | import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; 4 | import basicAuth from 'express-basic-auth'; 5 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 6 | import { getMetadataArgsStorage } from 'routing-controllers'; 7 | import { routingControllersToSpec } from 'routing-controllers-openapi'; 8 | import * as swaggerUi from 'swagger-ui-express'; 9 | 10 | import { env } from '../env'; 11 | 12 | export const swaggerLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 13 | if (settings && env.swagger.enabled) { 14 | const expressApp = settings.getData('express_app'); 15 | 16 | const { validationMetadatas } = getFromContainer( 17 | MetadataStorage 18 | ) as any; 19 | 20 | const schemas = validationMetadatasToSchemas(validationMetadatas, { 21 | classTransformerMetadataStorage, 22 | refPointerPrefix: '#/components/schemas/', 23 | }); 24 | 25 | const swaggerFile = routingControllersToSpec( 26 | getMetadataArgsStorage(), 27 | {}, 28 | { 29 | components: { 30 | schemas, 31 | securitySchemes: { 32 | basicAuth: { 33 | type: 'http', 34 | scheme: 'basic', 35 | }, 36 | }, 37 | }, 38 | } 39 | ); 40 | 41 | // Add npm infos to the swagger doc 42 | swaggerFile.info = { 43 | title: env.app.name, 44 | description: env.app.description, 45 | version: env.app.version, 46 | }; 47 | 48 | swaggerFile.servers = [ 49 | { 50 | url: `${env.app.schema}://${env.app.host}:${env.app.port}${env.app.routePrefix}`, 51 | }, 52 | ]; 53 | 54 | expressApp.use( 55 | env.swagger.route, 56 | env.swagger.username ? basicAuth({ 57 | users: { 58 | [`${env.swagger.username}`]: env.swagger.password, 59 | }, 60 | challenge: true, 61 | }) : (req, res, next) => next(), 62 | swaggerUi.serve, 63 | swaggerUi.setup(swaggerFile) 64 | ); 65 | 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/loaders/typeormLoader.ts: -------------------------------------------------------------------------------- 1 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 2 | import { createConnection, getConnectionOptions } from 'typeorm'; 3 | 4 | import { env } from '../env'; 5 | 6 | export const typeormLoader: MicroframeworkLoader = async (settings: MicroframeworkSettings | undefined) => { 7 | 8 | const loadedConnectionOptions = await getConnectionOptions(); 9 | 10 | const connectionOptions = Object.assign(loadedConnectionOptions, { 11 | type: env.db.type as any, // See createConnection options for valid types 12 | host: env.db.host, 13 | port: env.db.port, 14 | username: env.db.username, 15 | password: env.db.password, 16 | database: env.db.database, 17 | synchronize: env.db.synchronize, 18 | logging: env.db.logging, 19 | entities: env.app.dirs.entities, 20 | migrations: env.app.dirs.migrations, 21 | }); 22 | 23 | const connection = await createConnection(connectionOptions); 24 | 25 | if (settings) { 26 | settings.setData('connection', connection); 27 | settings.onShutdown(() => connection.close()); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/loaders/winstonLoader.ts: -------------------------------------------------------------------------------- 1 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 2 | import { configure, format, transports } from 'winston'; 3 | 4 | import { env } from '../env'; 5 | 6 | export const winstonLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 7 | configure({ 8 | transports: [ 9 | new transports.Console({ 10 | level: env.log.level, 11 | handleExceptions: true, 12 | format: env.node !== 'development' 13 | ? format.combine( 14 | format.json() 15 | ) 16 | : format.combine( 17 | format.colorize(), 18 | format.simple() 19 | ), 20 | }), 21 | ], 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tecch/express-typescript-boilerplate/17727010f7f098d8ca8b78da0a5b3c7297659381/src/public/favicon.ico -------------------------------------------------------------------------------- /src/types/json.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/api/info.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import { env } from '../../../src/env'; 4 | import { bootstrapApp, BootstrapSettings } from '../utils/bootstrap'; 5 | 6 | describe('/api', () => { 7 | 8 | // ------------------------------------------------------------------------- 9 | // Setup up 10 | // ------------------------------------------------------------------------- 11 | 12 | let settings: BootstrapSettings; 13 | beforeAll(async () => settings = await bootstrapApp()); 14 | 15 | // ------------------------------------------------------------------------- 16 | // Test cases 17 | // ------------------------------------------------------------------------- 18 | 19 | test('GET: / should return the api-version', async (done) => { 20 | const response = await request(settings.app) 21 | .get('/api') 22 | .expect('Content-Type', /json/) 23 | .expect(200); 24 | 25 | expect(response.body.version).toBe(env.app.version); 26 | done(); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/e2e/api/users.test.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | import request from 'supertest'; 3 | import { runSeed } from 'typeorm-seeding'; 4 | 5 | import { User } from '../../../src/api/models/User'; 6 | import { CreateBruce } from '../../../src/database/seeds/CreateBruce'; 7 | import { closeDatabase } from '../../utils/database'; 8 | import { BootstrapSettings } from '../utils/bootstrap'; 9 | import { prepareServer } from '../utils/server'; 10 | 11 | describe('/api/users', () => { 12 | 13 | let bruce: User; 14 | let bruceAuthorization: string; 15 | let settings: BootstrapSettings; 16 | 17 | // ------------------------------------------------------------------------- 18 | // Setup up 19 | // ------------------------------------------------------------------------- 20 | 21 | beforeAll(async () => { 22 | settings = await prepareServer({ migrate: true }); 23 | bruce = await runSeed(CreateBruce); 24 | bruceAuthorization = Buffer.from(`${bruce.username}:1234`).toString('base64'); 25 | }); 26 | 27 | // ------------------------------------------------------------------------- 28 | // Tear down 29 | // ------------------------------------------------------------------------- 30 | 31 | afterAll(async () => { 32 | nock.cleanAll(); 33 | await closeDatabase(settings.connection); 34 | }); 35 | 36 | // ------------------------------------------------------------------------- 37 | // Test cases 38 | // ------------------------------------------------------------------------- 39 | 40 | test('GET: / should return a list of users', async (done) => { 41 | const response = await request(settings.app) 42 | .get('/api/users') 43 | .set('Authorization', `Basic ${bruceAuthorization}`) 44 | .expect('Content-Type', /json/) 45 | .expect(200); 46 | 47 | expect(response.body.length).toBe(1); 48 | done(); 49 | }); 50 | 51 | test('GET: /:id should return bruce', async (done) => { 52 | const response = await request(settings.app) 53 | .get(`/api/users/${bruce.id}`) 54 | .set('Authorization', `Basic ${bruceAuthorization}`) 55 | .expect('Content-Type', /json/) 56 | .expect(200); 57 | 58 | expect(response.body.id).toBe(bruce.id); 59 | expect(response.body.firstName).toBe(bruce.firstName); 60 | expect(response.body.lastName).toBe(bruce.lastName); 61 | expect(response.body.email).toBe(bruce.email); 62 | done(); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/e2e/utils/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'express'; 2 | import * as http from 'http'; 3 | import { bootstrapMicroframework } from 'microframework-w3tec'; 4 | import { Connection } from 'typeorm/connection/Connection'; 5 | 6 | import { eventDispatchLoader } from '../../../src/loaders/eventDispatchLoader'; 7 | import { expressLoader } from '../../../src/loaders/expressLoader'; 8 | import { homeLoader } from '../../../src/loaders/homeLoader'; 9 | import { iocLoader } from '../../../src/loaders/iocLoader'; 10 | import { winstonLoader } from '../../../src/loaders/winstonLoader'; 11 | import { typeormLoader } from '../utils/typeormLoader'; 12 | 13 | export interface BootstrapSettings { 14 | app: Application; 15 | server: http.Server; 16 | connection: Connection; 17 | } 18 | 19 | export const bootstrapApp = async (): Promise => { 20 | const framework = await bootstrapMicroframework({ 21 | loaders: [ 22 | winstonLoader, 23 | iocLoader, 24 | eventDispatchLoader, 25 | typeormLoader, 26 | expressLoader, 27 | homeLoader, 28 | ], 29 | }); 30 | return { 31 | app: framework.settings.getData('express_app') as Application, 32 | server: framework.settings.getData('express_server') as http.Server, 33 | connection: framework.settings.getData('connection') as Connection, 34 | } as BootstrapSettings; 35 | }; 36 | -------------------------------------------------------------------------------- /test/e2e/utils/server.ts: -------------------------------------------------------------------------------- 1 | import { setConnection } from 'typeorm-seeding'; 2 | 3 | import { migrateDatabase } from '../../utils/database'; 4 | import { bootstrapApp } from './bootstrap'; 5 | 6 | export const prepareServer = async (options?: { migrate: boolean }) => { 7 | const settings = await bootstrapApp(); 8 | if (options && options.migrate) { 9 | await migrateDatabase(settings.connection); 10 | } 11 | setConnection(settings.connection); 12 | return settings; 13 | }; 14 | -------------------------------------------------------------------------------- /test/e2e/utils/typeormLoader.ts: -------------------------------------------------------------------------------- 1 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 2 | 3 | import { createDatabaseConnection } from '../../utils/database'; 4 | 5 | export const typeormLoader: MicroframeworkLoader = async (settings: MicroframeworkSettings | undefined) => { 6 | 7 | const connection = await createDatabaseConnection(); 8 | if (settings) { 9 | settings.setData('connection', connection); 10 | settings.onShutdown(() => connection.close()); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /test/integration/PetService.test.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'typedi'; 2 | import { Connection } from 'typeorm'; 3 | 4 | import { Pet } from '../../src/api/models/Pet'; 5 | import { PetService } from '../../src/api/services/PetService'; 6 | import { closeDatabase, createDatabaseConnection, migrateDatabase } from '../utils/database'; 7 | import { configureLogger } from '../utils/logger'; 8 | 9 | describe('PetService', () => { 10 | 11 | // ------------------------------------------------------------------------- 12 | // Setup up 13 | // ------------------------------------------------------------------------- 14 | 15 | let connection: Connection; 16 | beforeAll(async () => { 17 | configureLogger(); 18 | connection = await createDatabaseConnection(); 19 | }); 20 | beforeEach(() => migrateDatabase(connection)); 21 | 22 | // ------------------------------------------------------------------------- 23 | // Tear down 24 | // ------------------------------------------------------------------------- 25 | 26 | afterAll(() => closeDatabase(connection)); 27 | 28 | // ------------------------------------------------------------------------- 29 | // Test cases 30 | // ------------------------------------------------------------------------- 31 | 32 | test('should create a new pet in the database', async (done) => { 33 | const pet = new Pet(); 34 | pet.id = 'xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx'; 35 | pet.name = 'test'; 36 | pet.age = 1; 37 | const service = Container.get(PetService); 38 | const resultCreate = await service.create(pet); 39 | expect(resultCreate.name).toBe(pet.name); 40 | expect(resultCreate.age).toBe(pet.age); 41 | 42 | const resultFind = await service.findOne(resultCreate.id); 43 | if (resultFind) { 44 | expect(resultFind.name).toBe(pet.name); 45 | expect(resultFind.age).toBe(pet.age); 46 | } else { 47 | fail('Could not find pet'); 48 | } 49 | done(); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/preprocessor.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | const tsc = require('typescript'); 4 | const tsConfig = require('./../tsconfig.json'); 5 | 6 | module.exports = { 7 | process(src, path) { 8 | if (path.endsWith('.ts') || path.endsWith('.tsx')) { 9 | return tsc.transpile(src, tsConfig.compilerOptions, path, []); 10 | } 11 | return src; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /test/unit/auth/AuthService.test.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import MockExpressRequest from 'mock-express-request'; 3 | import { User } from 'src/api/models/User'; 4 | 5 | import { AuthService } from '../../../src/auth/AuthService'; 6 | import { LogMock } from '../lib/LogMock'; 7 | import { RepositoryMock } from '../lib/RepositoryMock'; 8 | 9 | describe('AuthService', () => { 10 | 11 | let authService: AuthService; 12 | let userRepository: RepositoryMock; 13 | let log: LogMock; 14 | beforeEach(() => { 15 | log = new LogMock(); 16 | userRepository = new RepositoryMock(); 17 | authService = new AuthService(log, userRepository as any); 18 | }); 19 | 20 | describe('parseTokenFromRequest', () => { 21 | test('Should return the credentials of the basic authorization header', () => { 22 | const base64 = Buffer.from(`bruce:1234`).toString('base64'); 23 | const req: Request = new MockExpressRequest({ 24 | headers: { 25 | Authorization: `Basic ${base64}`, 26 | }, 27 | }); 28 | const credentials = authService.parseBasicAuthFromRequest(req); 29 | expect(credentials.username).toBe('bruce'); 30 | expect(credentials.password).toBe('1234'); 31 | }); 32 | 33 | test('Should return undefined if there is no basic authorization header', () => { 34 | const req: Request = new MockExpressRequest({ 35 | headers: {}, 36 | }); 37 | const token = authService.parseBasicAuthFromRequest(req); 38 | expect(token).toBeUndefined(); 39 | expect(log.infoMock).toBeCalledWith('No credentials provided by the client', []); 40 | }); 41 | 42 | test('Should return undefined if there is a invalid basic authorization header', () => { 43 | const req: Request = new MockExpressRequest({ 44 | headers: { 45 | Authorization: 'Basic 1234', 46 | }, 47 | }); 48 | const token = authService.parseBasicAuthFromRequest(req); 49 | expect(token).toBeUndefined(); 50 | expect(log.infoMock).toBeCalledWith('No credentials provided by the client', []); 51 | }); 52 | 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /test/unit/lib/EventDispatcherMock.ts: -------------------------------------------------------------------------------- 1 | export class EventDispatcherMock { 2 | 3 | public dispatchMock = jest.fn(); 4 | 5 | public dispatch(...args: any[]): void { 6 | this.dispatchMock(args); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/lib/LogMock.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../../../src/lib/logger'; 2 | 3 | export class LogMock extends Logger { 4 | 5 | public debugMock = jest.fn(); 6 | public infoMock = jest.fn(); 7 | public warnMock = jest.fn(); 8 | public errorMock = jest.fn(); 9 | 10 | public debug(message: string, ...args: any[]): void { 11 | this.debugMock(message, args); 12 | } 13 | 14 | public info(message: string, ...args: any[]): void { 15 | this.infoMock(message, args); 16 | } 17 | 18 | public warn(message: string, ...args: any[]): void { 19 | this.warnMock(message, args); 20 | } 21 | 22 | public error(message: string, ...args: any[]): void { 23 | this.errorMock(message, args); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/lib/RepositoryMock.ts: -------------------------------------------------------------------------------- 1 | export class RepositoryMock { 2 | 3 | public one: T; 4 | public list: T[]; 5 | 6 | public findMock = jest.fn(); 7 | public findOneMock = jest.fn(); 8 | public saveMock = jest.fn(); 9 | public deleteMock = jest.fn(); 10 | 11 | public find(...args: any[]): Promise { 12 | this.findMock(args); 13 | return Promise.resolve(this.list); 14 | } 15 | 16 | public findOne(...args: any[]): Promise { 17 | this.findOneMock(args); 18 | return Promise.resolve(this.one); 19 | } 20 | 21 | public save(value: T, ...args: any[]): Promise { 22 | this.saveMock(value, args); 23 | return Promise.resolve(value); 24 | } 25 | 26 | public delete(value: T, ...args: any[]): Promise { 27 | this.deleteMock(value, args); 28 | return Promise.resolve(value); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /test/unit/lib/setup.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | -------------------------------------------------------------------------------- /test/unit/middlewares/ErrorHandlerMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import MockExpressResponse from 'mock-express-response'; 2 | import { HttpError } from 'routing-controllers'; 3 | 4 | import { ErrorHandlerMiddleware } from '../../../src/api/middlewares/ErrorHandlerMiddleware'; 5 | import { LogMock } from '../lib/LogMock'; 6 | 7 | describe('ErrorHandlerMiddleware', () => { 8 | 9 | let log; 10 | let middleware; 11 | let err; 12 | let res; 13 | beforeEach(() => { 14 | log = new LogMock(); 15 | middleware = new ErrorHandlerMiddleware(log); 16 | res = new MockExpressResponse(); 17 | err = new HttpError(400, 'Test Error'); 18 | }); 19 | 20 | test('Should not print stack out in production', () => { 21 | middleware.isProduction = true; 22 | middleware.error(err, undefined, res, undefined); 23 | const json = res._getJSON(); 24 | expect(json.name).toBe(err.name); 25 | expect(json.message).toBe(err.message); 26 | expect(log.errorMock).toHaveBeenCalledWith(err.name, [err.message]); 27 | }); 28 | 29 | test('Should print stack out in development', () => { 30 | middleware.isProduction = false; 31 | middleware.error(err, undefined, res, undefined); 32 | const json = res._getJSON(); 33 | expect(json.name).toBe(err.name); 34 | expect(json.message).toBe(err.message); 35 | expect(log.errorMock).toHaveBeenCalled(); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/services/UserService.test.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../../src/api/models/User'; 2 | import { UserService } from '../../../src/api/services/UserService'; 3 | import { events } from '../../../src/api/subscribers/events'; 4 | import { EventDispatcherMock } from '../lib/EventDispatcherMock'; 5 | import { LogMock } from '../lib/LogMock'; 6 | import { RepositoryMock } from '../lib/RepositoryMock'; 7 | 8 | describe('UserService', () => { 9 | 10 | test('Find should return a list of users', async (done) => { 11 | const log = new LogMock(); 12 | const repo = new RepositoryMock(); 13 | const ed = new EventDispatcherMock(); 14 | const user = new User(); 15 | user.id = '1'; 16 | user.firstName = 'John'; 17 | user.lastName = 'Doe'; 18 | user.email = 'john.doe@test.com'; 19 | repo.list = [user]; 20 | const userService = new UserService(repo as any, ed as any, log); 21 | const list = await userService.find(); 22 | expect(list[0].firstName).toBe(user.firstName); 23 | done(); 24 | }); 25 | 26 | test('Create should dispatch subscribers', async (done) => { 27 | const log = new LogMock(); 28 | const repo = new RepositoryMock(); 29 | const ed = new EventDispatcherMock(); 30 | const user = new User(); 31 | user.id = '1'; 32 | user.firstName = 'John'; 33 | user.lastName = 'Doe'; 34 | user.email = 'john.doe@test.com'; 35 | const userService = new UserService(repo as any, ed as any, log); 36 | const newUser = await userService.create(user); 37 | expect(ed.dispatchMock).toBeCalledWith([events.user.created, newUser]); 38 | done(); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/validations/UserValidations.test.ts: -------------------------------------------------------------------------------- 1 | import { validate } from 'class-validator'; 2 | 3 | import { User } from '../../../src/api/models/User'; 4 | 5 | describe('UserValidations', () => { 6 | 7 | test('User should always have a first name', async (done) => { 8 | const user = new User(); 9 | const errorsOne = await validate(user); 10 | user.firstName = 'TestName'; 11 | const errorsTwo = await validate(user); 12 | expect(errorsOne.length).toBeGreaterThan(errorsTwo.length); 13 | done(); 14 | }); 15 | 16 | test('User should always have a last name', async (done) => { 17 | const user = new User(); 18 | const errorsOne = await validate(user); 19 | user.lastName = 'TestName'; 20 | const errorsTwo = await validate(user); 21 | expect(errorsOne.length).toBeGreaterThan(errorsTwo.length); 22 | done(); 23 | }); 24 | 25 | test('User should always have a email', async (done) => { 26 | const user = new User(); 27 | const errorsOne = await validate(user); 28 | user.email = 'test@test.com'; 29 | const errorsTwo = await validate(user); 30 | expect(errorsOne.length).toBeGreaterThan(errorsTwo.length); 31 | done(); 32 | }); 33 | 34 | test('User validation should succeed with all required fields', async (done) => { 35 | const user = new User(); 36 | user.firstName = 'TestName'; 37 | user.lastName = 'TestName'; 38 | user.email = 'test@test.com'; 39 | user.username = 'test'; 40 | user.password = '1234'; 41 | const errors = await validate(user); 42 | expect(errors.length).toEqual(0); 43 | done(); 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /test/utils/database.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'typedi'; 2 | import { Connection, createConnection, useContainer } from 'typeorm'; 3 | 4 | import { env } from '../../src/env'; 5 | 6 | declare type LoggerOptions = boolean | 'all' | Array<('query' | 'schema' | 'error' | 'warn' | 'info' | 'log' | 'migration')>; 7 | 8 | export const createDatabaseConnection = async (): Promise => { 9 | useContainer(Container); 10 | const connection = await createConnection({ 11 | type: env.db.type as any, // See createConnection options for valid types 12 | database: env.db.database, 13 | logging: env.db.logging as LoggerOptions, 14 | entities: env.app.dirs.entities, 15 | migrations: env.app.dirs.migrations, 16 | }); 17 | return connection; 18 | }; 19 | 20 | export const synchronizeDatabase = async (connection: Connection) => { 21 | await connection.dropDatabase(); 22 | return connection.synchronize(true); 23 | }; 24 | 25 | export const migrateDatabase = async (connection: Connection) => { 26 | await connection.dropDatabase(); 27 | return connection.runMigrations(); 28 | }; 29 | 30 | export const closeDatabase = (connection: Connection) => { 31 | return connection.close(); 32 | }; 33 | -------------------------------------------------------------------------------- /test/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { configure, transports } from 'winston'; 2 | 3 | export const configureLogger = () => { 4 | configure({ 5 | transports: [ 6 | new transports.Console({ 7 | level: 'none', 8 | handleExceptions: false, 9 | }), 10 | ], 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "pretty": true, 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "importHelpers": true, 9 | "strict": true, 10 | "noImplicitAny": false, 11 | "strictNullChecks": false, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "moduleResolution": "node", 19 | "baseUrl": ".", 20 | "allowSyntheticDefaultImports": true, 21 | "experimentalDecorators": true, 22 | "emitDecoratorMetadata": true, 23 | "resolveJsonModule": true, 24 | "esModuleInterop": true, 25 | "lib": [ 26 | "es5", 27 | "es6", 28 | "dom", 29 | "es2015.core", 30 | "es2015.collection", 31 | "es2015.generator", 32 | "es2015.iterable", 33 | "es2015.promise", 34 | "es2015.proxy", 35 | "es2015.reflect", 36 | "es2015.symbol", 37 | "es2015.symbol.wellknown", 38 | "esnext.asynciterable" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "max-classes-per-file": false, 5 | "max-line-length": [ 6 | true, 7 | 160 8 | ], 9 | "no-unnecessary-initializer": false, 10 | "no-var-requires": true, 11 | "no-null-keyword": true, 12 | "no-consecutive-blank-lines": true, 13 | "quotemark": [ 14 | true, 15 | "single", 16 | "avoid-escape" 17 | ], 18 | "interface-name": false, 19 | "no-empty-interface": false, 20 | "no-namespace": false, 21 | "ordered-imports": false, 22 | "object-literal-sort-keys": false, 23 | "arrow-parens": false, 24 | "member-ordering": [ 25 | true, 26 | { 27 | "order": [ 28 | "public-static-field", 29 | "public-static-method", 30 | "protected-static-field", 31 | "protected-static-method", 32 | "private-static-field", 33 | "private-static-method", 34 | "public-instance-field", 35 | "protected-instance-field", 36 | "private-instance-field", 37 | "public-constructor", 38 | "protected-constructor", 39 | "private-constructor", 40 | "public-instance-method", 41 | "protected-instance-method", 42 | "private-instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-inferrable-types": [ 55 | true, 56 | "ignore-params" 57 | ], 58 | "no-switch-case-fall-through": true, 59 | "typedef": [ 60 | true, 61 | "call-signature", 62 | "parameter" 63 | ], 64 | "trailing-comma": [ 65 | true, 66 | { 67 | "multiline": { 68 | "objects": "always", 69 | "arrays": "always", 70 | "functions": "never", 71 | "typeLiterals": "ignore" 72 | }, 73 | "singleline": "never" 74 | } 75 | ], 76 | "align": [ 77 | true, 78 | "parameters" 79 | ], 80 | "class-name": true, 81 | "curly": true, 82 | "eofline": true, 83 | "jsdoc-format": true, 84 | "member-access": true, 85 | "no-arg": true, 86 | "no-construct": true, 87 | "no-duplicate-variable": true, 88 | "no-empty": true, 89 | "no-eval": true, 90 | "no-internal-module": true, 91 | "no-string-literal": true, 92 | "no-trailing-whitespace": true, 93 | "no-unused-expression": true, 94 | "no-var-keyword": true, 95 | "one-line": [ 96 | true, 97 | "check-open-brace", 98 | "check-catch", 99 | "check-else", 100 | "check-finally", 101 | "check-whitespace" 102 | ], 103 | "semicolon": true, 104 | "switch-default": true, 105 | "triple-equals": [ 106 | true, 107 | "allow-null-check" 108 | ], 109 | "typedef-whitespace": [ 110 | true, 111 | { 112 | "call-signature": "nospace", 113 | "index-signature": "nospace", 114 | "parameter": "nospace", 115 | "property-declaration": "nospace", 116 | "variable-declaration": "nospace" 117 | } 118 | ], 119 | "variable-name": false, 120 | "whitespace": [ 121 | true, 122 | "check-branch", 123 | "check-decl", 124 | "check-operator", 125 | "check-separator", 126 | "check-type" 127 | ] 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /w3tec-divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tecch/express-typescript-boilerplate/17727010f7f098d8ca8b78da0a5b3c7297659381/w3tec-divider.png -------------------------------------------------------------------------------- /w3tec-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3tecch/express-typescript-boilerplate/17727010f7f098d8ca8b78da0a5b3c7297659381/w3tec-logo.png --------------------------------------------------------------------------------