├── .dockerignore ├── .editorconfig ├── .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 ├── ormconfig.ts └── tsconfig.ts ├── icon.png ├── nodemon.json ├── package-scripts.js ├── package.json ├── src ├── api │ ├── 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 │ ├── mutations │ │ └── CreatePetMutation.ts │ ├── queries │ │ ├── GetPetsQuery.ts │ │ ├── userQuery.ts │ │ └── usersQuery.ts │ ├── repositories │ │ ├── PetRepository.ts │ │ └── UserRepository.ts │ ├── resultModels │ │ └── UserCustorResult.ts │ ├── resultTypes │ │ └── userCursorResult.ts │ ├── services │ │ ├── PetService.ts │ │ └── UserService.ts │ ├── subscribers │ │ ├── UserEventSubscriber.ts │ │ └── events.ts │ ├── swagger.json │ ├── types │ │ ├── PetType.ts │ │ ├── UserType.ts │ │ └── cursorFilterType.ts │ └── validators │ │ └── .gitkeep ├── app.ts ├── auth │ ├── AuthService.ts │ ├── TokenInfoInterface.ts │ ├── authorizationChecker.ts │ └── currentUserChecker.ts ├── database │ ├── Copia de migrations │ │ ├── 1511105183653-CreateUserTable.ts │ │ ├── 1512663524808-CreatePetTable.ts │ │ └── 1512663990063-AddUserRelationToPetTable.ts │ ├── factories │ │ ├── PetFactory.ts │ │ └── UserFactory.ts │ └── seeds │ │ ├── CreateBruce.ts │ │ ├── CreatePets.ts │ │ └── CreateUsers.ts ├── decorators │ ├── EventDispatcher.ts │ └── Logger.ts ├── env.ts ├── lib │ ├── banner.ts │ ├── env │ │ ├── index.ts │ │ └── utils.ts │ ├── graphql │ │ ├── AbstractGraphQLHooks.ts │ │ ├── AbstractGraphQLMutation.ts │ │ ├── AbstractGraphQLQuery.ts │ │ ├── GraphQLContext.ts │ │ ├── MetadataArgsStorage.ts │ │ ├── Mutation.ts │ │ ├── MutationMetadataArgs.ts │ │ ├── Query.ts │ │ ├── QueryMetadataArgs.ts │ │ ├── container.ts │ │ ├── graphql-error-handling.ts │ │ ├── importClassesFromDirectories.ts │ │ └── index.ts │ ├── logger │ │ ├── Logger.ts │ │ ├── LoggerInterface.ts │ │ └── index.ts │ └── seed │ │ ├── EntityFactory.ts │ │ ├── cli.ts │ │ ├── connection.ts │ │ ├── importer.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.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 │ │ ├── auth.ts │ │ ├── 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 ├── 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.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_JSON=false 16 | LOG_OUTPUT=dev 17 | 18 | # 19 | # AUTHORIZATION 20 | # 21 | AUTH_ROUTE=http://localhost:3333/tokeninfo 22 | 23 | # 24 | # DATABASE 25 | # 26 | TYPEORM_CONNECTION=sqlite 27 | TYPEORM_DATABASE=./mydb.sql 28 | TYPEORM_LOGGING=false 29 | 30 | # 31 | # GraphQL 32 | # 33 | GRAPHQL_ENABLED=true 34 | GRAPHQL_ROUTE=/graphql 35 | 36 | # 37 | # Swagger 38 | # 39 | SWAGGER_ENABLED=true 40 | SWAGGER_ROUTE=/swagger 41 | SWAGGER_FILE=api/swagger.json 42 | SWAGGER_USERNAME=admin 43 | SWAGGER_PASSWORD=1234 44 | 45 | # 46 | # Status Monitor 47 | # 48 | MONITOR_ENABLED=true 49 | MONITOR_ROUTE=/monitor 50 | MONITOR_USERNAME=admin 51 | MONITOR_PASSWORD=1234 52 | -------------------------------------------------------------------------------- /.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 | 18 | # Typing # 19 | typings/ 20 | 21 | # Dist # 22 | dist/ 23 | ormconfig.json 24 | tsconfig.build.json 25 | 26 | # IDE # 27 | .idea/ 28 | *.swp 29 | .awcache 30 | 31 | # Generated source-code # 32 | src/**/*.js 33 | src/**/*.js.map 34 | !src/public/**/* 35 | test/**/*.js 36 | test/**/*.js.map 37 | coverage/ 38 | !test/preprocessor.js 39 | mydb.sql 40 | -------------------------------------------------------------------------------- /.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": "development" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "cSpell.enabled": true, 4 | "files.exclude": { 5 | "tsconfig.build.json": true, 6 | "ormconfig.json": true 7 | }, 8 | "importSorter.generalConfiguration.sortOnBeforeSave": true, 9 | "files.trimTrailingWhitespace": true, 10 | "editor.formatOnSave": false 11 | } 12 | -------------------------------------------------------------------------------- /.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/). 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 | - **DataLoaders** helps with performance thanks to caching and batching [DataLoaders](https://github.com/facebook/dataloader). 59 | 60 | ![divider](./w3tec-divider.png) 61 | 62 | ## ❯ Table of Contents 63 | 64 | - [Getting Started](#-getting-started) 65 | - [Scripts and Tasks](#-scripts-and-tasks) 66 | - [Debugger in VSCode](#-debugger-in-vscode) 67 | - [API Routes](#-api-routes) 68 | - [Project Structure](#-project-structure) 69 | - [Logging](#-logging) 70 | - [Event Dispatching](#-event-dispatching) 71 | - [Seeding](#-seeding) 72 | - [Further Documentations](#-further-documentation) 73 | - [Related Projects](#-related-projects) 74 | - [License](#-license) 75 | 76 | ![divider](./w3tec-divider.png) 77 | 78 | ## ❯ Getting Started 79 | 80 | ### Step 1: Set up the Development Environment 81 | 82 | You need to set up your development environment before you can do anything. 83 | 84 | Install [Node.js and NPM](https://nodejs.org/en/download/) 85 | 86 | - on OSX use [homebrew](http://brew.sh) `brew install node` 87 | - on Windows use [chocolatey](https://chocolatey.org/) `choco install nodejs` 88 | 89 | Install yarn globally 90 | 91 | ```bash 92 | npm install yarn -g 93 | ``` 94 | 95 | Install a MySQL database. 96 | 97 | > If you work with a mac, we recommend to use homebrew for the installation. 98 | 99 | ### Step 2: Create new Project 100 | 101 | Fork or download this project. Configure your package.json for your new project. 102 | 103 | Then copy the `.env.example` file and rename it to `.env`. In this file you have to add your database connection information. 104 | 105 | Create a new database with the name you have in your `.env`-file. 106 | 107 | Then setup your application environment. 108 | 109 | ```bash 110 | yarn setup 111 | ``` 112 | 113 | > 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. 114 | 115 | ### Step 3: Serve your App 116 | 117 | Go to the project dir and start your app with this npm script. 118 | 119 | ```bash 120 | yarn start serve 121 | ``` 122 | 123 | > This starts a local server using `nodemon`, which will watch for any file changes and will restart the sever according to these changes. 124 | > The server address will be displayed to you as `http://0.0.0.0:3000`. 125 | 126 | ![divider](./w3tec-divider.png) 127 | 128 | ## ❯ Scripts and Tasks 129 | 130 | All script are defined in the `package-scripts.js` file, but the most important ones are listed here. 131 | 132 | ### Install 133 | 134 | - Install all dependencies with `yarn install` 135 | 136 | ### Linting 137 | 138 | - Run code quality analysis using `yarn start lint`. This runs tslint. 139 | - There is also a vscode task for this called `lint`. 140 | 141 | ### Tests 142 | 143 | - Run the unit tests using `yarn start test` (There is also a vscode task for this called `test`). 144 | - Run the integration tests using `yarn start test.integration`. 145 | - Run the e2e tests using `yarn start test.e2e`. 146 | 147 | ### Running in dev mode 148 | 149 | - Run `yarn start serve` to start nodemon with ts-node, to serve the app. 150 | - The server address will be displayed to you as `http://0.0.0.0:3000` 151 | 152 | ### Building the project and run it 153 | 154 | - Run `yarn start build` to generated all JavaScript files from the TypeScript sources (There is also a vscode task for this called `build`). 155 | - To start the builded app located in `dist` use `yarn start`. 156 | 157 | ### Database Migration 158 | 159 | - Run `typeorm migration:create -n ` to create a new migration file. 160 | - Try `typeorm -h` to see more useful cli commands like generating migration out of your models. 161 | - To migrate your database run `yarn start db.migrate`. 162 | - To revert your latest migration run `yarn start db.revert`. 163 | - Drops the complete database schema `yarn start db.drop`. 164 | 165 | ### Database Seeding 166 | 167 | - Run `yarn start db.seed` to seed your seeds into the database. 168 | 169 | ![divider](./w3tec-divider.png) 170 | 171 | ## ❯ Debugger in VSCode 172 | 173 | To debug your code run `yarn start build` or hit cmd + b to build your app. 174 | Then, just set a breakpoint and hit F5 in your Visual Studio Code. 175 | 176 | ![divider](./w3tec-divider.png) 177 | 178 | ## ❯ API Routes 179 | 180 | The route prefix is `/api` by default, but you can change this in the .env file. 181 | The swagger and the monitor route can be altered in the `.env` file. 182 | 183 | | Route | Description | 184 | | -------------- | ----------- | 185 | | **/api** | Shows us the name, description and the version of the package.json | 186 | | **/graphql** | Route to the graphql editor or your query/mutations requests | 187 | | **/swagger** | This is the Swagger UI with our API documentation | 188 | | **/monitor** | Shows a small monitor page for the server | 189 | | **/api/users** | Example entity endpoint | 190 | | **/api/pets** | Example entity endpoint | 191 | 192 | ![divider](./w3tec-divider.png) 193 | 194 | ## ❯ Project Structure 195 | 196 | | Name | Description | 197 | | --------------------------------- | ----------- | 198 | | **.vscode/** | VSCode tasks, launch configuration and some other settings | 199 | | **dist/** | Compiled source files will be placed here | 200 | | **src/** | Source files | 201 | | **src/api/controllers/** | REST API Controllers | 202 | | **src/api/controllers/requests** | Request classes with validation rules if the body is not equal with a model | 203 | | **src/api/controllers/responses** | Response classes or interfaces to type json response bodies | 204 | | **src/api/errors/** | Custom HttpErrors like 404 NotFound | 205 | | **src/api/interceptors/** | Interceptors are used to change or replace the data returned to the client. | 206 | | **src/api/middlewares/** | Express Middlewares like helmet security features | 207 | | **src/api/models/** | Bookshelf Models | 208 | | **src/api/repositories/** | Repository / DB layer | 209 | | **src/api/services/** | Service layer | 210 | | **src/api/subscribers/** | Event subscribers | 211 | | **src/api/validators/** | Custom validators, which can be used in the request classes | 212 | | **src/api/queries/** | GraphQL queries | 213 | | **src/api/mutations/** | GraphQL mutations | 214 | | **src/api/types/** | GraphQL types | 215 | | **src/api/** swagger.json | Swagger documentation | 216 | | **src/auth/** | Authentication checkers and services | 217 | | **src/core/** | The core features like logger and env variables | 218 | | **src/database/factories** | Factory the generate fake entities | 219 | | **src/database/migrations** | Database migration scripts | 220 | | **src/database/seeds** | Seeds to create some data in the database | 221 | | **src/decorators/** | Custom decorators like @Logger & @EventDispatch | 222 | | **src/loaders/** | Loader is a place where you can configure your app | 223 | | **src/public/** | Static assets (fonts, css, js, img). | 224 | | **src/types/** *.d.ts | Custom type definitions and files that aren't on DefinitelyTyped | 225 | | **test** | Tests | 226 | | **test/e2e/** *.test.ts | End-2-End tests (like e2e) | 227 | | **test/integration/** *.test.ts | Integration test with SQLite3 | 228 | | **test/unit/** *.test.ts | Unit tests | 229 | | .env.example | Environment configurations | 230 | | .env.test | Test environment configurations | 231 | | ormconfig.json | TypeORM configuration for the database. Used by seeds and the migration. (generated file) | 232 | | mydb.sql | SQLite database for integration tests. Ignored by git and only available after integration tests | 233 | 234 | ![divider](./w3tec-divider.png) 235 | 236 | ## ❯ Logging 237 | 238 | Our logger is [winston](https://github.com/winstonjs/winston). To log http request we use the express middleware [morgan](https://github.com/expressjs/morgan). 239 | We created a simple annotation to inject the logger in your service (see example below). 240 | 241 | ```typescript 242 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 243 | 244 | @Service() 245 | export class UserService { 246 | 247 | constructor( 248 | @Logger(__filename) private log: LoggerInterface 249 | ) { } 250 | 251 | ... 252 | ``` 253 | 254 | ![divider](./w3tec-divider.png) 255 | 256 | ## ❯ Event Dispatching 257 | 258 | We use this awesome repository [event-dispatch](https://github.com/pleerock/event-dispatch) for event dispatching. 259 | We created a simple annotation to inject the EventDispatcher in your service (see example below). All events are listed in the `events.ts` file. 260 | 261 | ```typescript 262 | import { events } from '../subscribers/events'; 263 | import { EventDispatcher, EventDispatcherInterface } from '../../decorators/EventDispatcher'; 264 | 265 | @Service() 266 | export class UserService { 267 | 268 | constructor( 269 | @EventDispatcher() private eventDispatcher: EventDispatcherInterface 270 | ) { } 271 | 272 | public async create(user: User): Promise { 273 | ... 274 | this.eventDispatcher.dispatch(events.user.created, newUser); 275 | ... 276 | } 277 | ``` 278 | 279 | ![divider](./w3tec-divider.png) 280 | 281 | ## ❯ Seeding 282 | 283 | Isn't it exhausting to create some sample data for your database, well this time is over! 284 | 285 | How does it work? Just create a factory for your entities (models) and a seed script. 286 | 287 | ### 1. Create a factory for your entity 288 | 289 | 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`. 290 | 291 | Settings can be used to pass some static value into the factory. 292 | 293 | ```typescript 294 | define(User, (faker: typeof Faker, settings: { roles: string[] }) => { 295 | const gender = faker.random.number(1); 296 | const firstName = faker.name.firstName(gender); 297 | const lastName = faker.name.lastName(gender); 298 | const email = faker.internet.email(firstName, lastName); 299 | 300 | const user = new User(); 301 | user.firstName = firstName; 302 | user.lastName = lastName; 303 | user.email = email; 304 | user.roles = settings.roles; 305 | return user; 306 | }); 307 | ``` 308 | 309 | Handle relation in the entity factory like this. 310 | 311 | ```typescript 312 | define(Pet, (faker: typeof Faker, settings: undefined) => { 313 | const gender = faker.random.number(1); 314 | const name = faker.name.firstName(gender); 315 | 316 | const pet = new Pet(); 317 | pet.name = name; 318 | pet.age = faker.random.number(); 319 | pet.user = factory(User)({ roles: ['admin'] }) 320 | return pet; 321 | }); 322 | ``` 323 | 324 | ### 2. Create a seed file 325 | 326 | The seeds files define how much and how the data are connected with each other. The files will be executed alphabetically. 327 | With the second function, accepting your settings defined in the factories, you are able to create different variations of entities. 328 | 329 | ```typescript 330 | export class CreateUsers implements Seed { 331 | 332 | public async seed(factory: Factory, connection: Connection): Promise { 333 | await factory(User)({ roles: [] }).createMany(10); 334 | } 335 | 336 | } 337 | ``` 338 | 339 | Here an example with nested factories. You can use the `.map()` function to alter 340 | the generated value before they get persisted. 341 | 342 | ```typescript 343 | ... 344 | await factory(User)() 345 | .map(async (user: User) => { 346 | const pets: Pet[] = await factory(Pet)().createMany(2); 347 | const petIds = pets.map((pet: Pet) => pet.Id); 348 | await user.pets().attach(petIds); 349 | }) 350 | .createMany(5); 351 | ... 352 | ``` 353 | 354 | To deal with relations you can use the entity manager like this. 355 | 356 | ```typescript 357 | export class CreatePets implements SeedsInterface { 358 | 359 | public async seed(factory: FactoryInterface, connection: Connection): Promise { 360 | const connection = await factory.getConnection(); 361 | const em = connection.createEntityManager(); 362 | 363 | await times(10, async (n) => { 364 | // This creates a pet in the database 365 | const pet = await factory(Pet)().create(); 366 | // This only returns a entity with fake data 367 | const user = await factory(User)({ roles: ['admin'] }).make(); 368 | user.pets = [pet]; 369 | await em.save(user); 370 | }); 371 | } 372 | 373 | } 374 | ``` 375 | 376 | ### 3. Run the seeder 377 | 378 | The last step is the easiest, just hit the following command in your terminal, but be sure you are in the projects root folder. 379 | 380 | ```bash 381 | yarn start db.seed 382 | ``` 383 | 384 | #### CLI Interface 385 | 386 | | Command | Description | 387 | | --------------------------------------------------- | ----------- | 388 | | `yarn start "db.seed"` | Run all seeds | 389 | | `yarn start "db.seed --run CreateBruce,CreatePets"` | Run specific seeds (file names without extension) | 390 | | `yarn start "db.seed -L"` | Log database queries to the terminal | 391 | | `yarn start "db.seed --factories "` | Add a different path to your factories (Default: `src/database/`) | 392 | | `yarn start "db.seed --seeds "` | Add a different path to your seeds (Default: `src/database/seeds/`) | 393 | | `yarn start "db.seed --config "` | Path to your ormconfig.json file | 394 | 395 | ![divider](./w3tec-divider.png) 396 | 397 | ## ❯ Run in Docker container 398 | 399 | ### Install Docker 400 | 401 | Before you start, make sure you have a recent version of [Docker](https://docs.docker.com/engine/installation/) installed 402 | 403 | ### Build Docker image 404 | 405 | ```shell 406 | docker build -t . 407 | ``` 408 | 409 | ### Run Docker image in container and map port 410 | 411 | 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`. 412 | 413 | #### Run image in detached mode 414 | 415 | ```shell 416 | docker run -d -p : 417 | ``` 418 | 419 | #### Run image in foreground mode 420 | 421 | ```shell 422 | docker run -i -t -p : 423 | ``` 424 | 425 | ### Stop Docker container 426 | 427 | #### Detached mode 428 | 429 | ```shell 430 | docker stop 431 | ``` 432 | 433 | You can get a list of all running Docker container and its ids by following command 434 | 435 | ```shell 436 | docker images 437 | ``` 438 | 439 | #### Foreground mode 440 | 441 | Go to console and press + C at any time. 442 | 443 | ### Docker environment variables 444 | 445 | There are several options to configure your app inside a Docker container 446 | 447 | #### project .env file 448 | 449 | 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. 450 | 451 | #### run options 452 | 453 | You can also change app configuration by passing environment variables via `docker run` option `-e` or `--env`. 454 | 455 | ```shell 456 | docker run --env DB_HOST=localhost -e DB_PORT=3306 457 | ``` 458 | 459 | #### environment file 460 | 461 | Last but not least you can pass a config file to `docker run`. 462 | 463 | ```shell 464 | docker run --env-file ./env.list 465 | ``` 466 | 467 | `env.list` example: 468 | 469 | ``` 470 | # this is a comment 471 | DB_TYPE=mysql 472 | DB_HOST=localhost 473 | DB_PORT=3306 474 | ``` 475 | 476 | ![divider](./w3tec-divider.png) 477 | 478 | ## ❯ Further Documentations 479 | 480 | | Name & Link | Description | 481 | | --------------------------------- | --------------------------------- | 482 | | [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. | 483 | | [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. | 484 | | [TypeDI](https://github.com/pleerock/typedi) | Dependency Injection for TypeScript. | 485 | | [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. | 486 | | [TypeORM](http://typeorm.io/#/) | TypeORM is highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework. | 487 | | [class-validator](https://github.com/pleerock/class-validator) | Validation made easy using TypeScript decorators. | 488 | | [class-transformer](https://github.com/pleerock/class-transformer) | Proper decorator-based transformation / serialization / deserialization of plain javascript objects to class constructors | 489 | | [event-dispatcher](https://github.com/pleerock/event-dispatch) | Dispatching and listening for application events in Typescript | 490 | | [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! | 491 | | [Auth0 API Documentation](https://auth0.com/docs/api/management/v2) | Authentification service | 492 | | [Jest](http://facebook.github.io/jest/) | Delightful JavaScript Testing Library for unit and e2e tests | 493 | | [supertest](https://github.com/visionmedia/supertest) | Super-agent driven library for testing node.js HTTP servers using a fluent API | 494 | | [nock](https://github.com/node-nock/nock) | HTTP mocking and expectations library | 495 | | [swagger Documentation](http://swagger.io/) | API Tool to describe and document your api. | 496 | | [SQLite Documentation](https://www.sitepoint.com/getting-started-sqlite3-basic-commands/) | Getting Started with SQLite3 – Basic Commands. | 497 | | [GraphQL Documentation](http://graphql.org/graphql-js/) | A query language for your API. | 498 | | [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. | 499 | 500 | ![divider](./w3tec-divider.png) 501 | 502 | ## ❯ Related Projects 503 | 504 | - [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. 505 | - [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 506 | - [aurelia-typescript-boilerplate](https://github.com/w3tecch/aurelia-typescript-boilerplate) - An Aurelia starter kit with TypeScript 507 | - [Auth0 Mock Server](https://github.com/hirsch88/auth0-mock-server) - Useful for e2e testing or faking an oAuth server 508 | 509 | ![divider](./w3tec-divider.png) 510 | 511 | ## ❯ License 512 | 513 | [MIT](/LICENSE) 514 | -------------------------------------------------------------------------------- /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(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/ormconfig.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as jsonfile from 'jsonfile'; 3 | import * as path from 'path'; 4 | 5 | import { env } from '../src/env'; 6 | 7 | dotenv.config(); 8 | 9 | const content = { 10 | type: env.db.type, 11 | host: env.db.host, 12 | port: env.db.port, 13 | username: env.db.username, 14 | password: env.db.password, 15 | database: env.db.database, 16 | entities: env.app.dirs.entities, 17 | migrations: env.app.dirs.migrations, 18 | cli: { 19 | migrationsDir: env.app.dirs.migrationsDir, 20 | }, 21 | }; 22 | 23 | const filePath = path.join(process.cwd(), 'ormconfig.json'); 24 | jsonfile.writeFile(filePath, content, { spaces: 2 }, (err) => { 25 | if (err === null) { 26 | process.exit(0); 27 | } else { 28 | console.error('Failed to generate the ormconfig.json', err); 29 | process.exit(1); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /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.include = [ 8 | 'src/**/*', 9 | ]; 10 | 11 | const filePath = path.join(process.cwd(), 'tsconfig.build.json'); 12 | jsonfile.writeFile(filePath, content, { spaces: 2 }, (err) => { 13 | if (err === null) { 14 | process.exit(0); 15 | } else { 16 | console.error('Failed to generate the tsconfig.build.json', err); 17 | process.exit(1); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeselaGen/typeorm-backend/2456e909f3a4b44d9be65e0a05d0eec1a23705b9/icon.png -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": "0", 3 | "execMap": { 4 | "ts": "ts-node" 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, crossEnv, concurrent, rimraf, runInNewWindow } = 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: 'node dist/app.js', 15 | description: 'Starts the builded app from the dist directory' 16 | }, 17 | /** 18 | * Serves the current app and watches for changes to restart it 19 | */ 20 | serve: { 21 | script: series( 22 | 'nps banner.serve', 23 | 'nodemon --watch src --watch .env' 24 | ), 25 | description: 'Serves the current app and watches for changes to restart it' 26 | }, 27 | /** 28 | * Setup of the development environment 29 | */ 30 | setup: { 31 | script: series( 32 | 'yarn install', 33 | 'nps db.drop', 34 | 'nps db.migrate', 35 | 'nps db.seed' 36 | ), 37 | description: 'Setup`s the development environment(yarn & database)' 38 | }, 39 | /** 40 | * Creates the needed configuration files 41 | */ 42 | config: { 43 | script: series( 44 | runFast('./commands/tsconfig.ts'), 45 | runFast('./commands/ormconfig.ts') 46 | ), 47 | hiddenFromHelp: true 48 | }, 49 | /** 50 | * Builds the app into the dist directory 51 | */ 52 | build: { 53 | script: series( 54 | 'nps banner.build', 55 | 'nps config', 56 | 'nps lint', 57 | 'nps clean.dist', 58 | 'nps transpile', 59 | 'nps copy' 60 | ), 61 | description: 'Builds the app into the dist directory' 62 | }, 63 | /** 64 | * Runs TSLint over your project 65 | */ 66 | lint: { 67 | script: tslint(`./src/**/*.ts`), 68 | hiddenFromHelp: true 69 | }, 70 | /** 71 | * Transpile your app into javascript 72 | */ 73 | transpile: { 74 | script: `tsc --project ./tsconfig.build.json`, 75 | hiddenFromHelp: true 76 | }, 77 | /** 78 | * Clean files and folders 79 | */ 80 | clean: { 81 | default: { 82 | script: series( 83 | `nps banner.clean`, 84 | `nps clean.dist` 85 | ), 86 | description: 'Deletes the ./dist folder' 87 | }, 88 | dist: { 89 | script: rimraf('./dist'), 90 | hiddenFromHelp: true 91 | } 92 | }, 93 | /** 94 | * Copies static files to the build folder 95 | */ 96 | copy: { 97 | default: { 98 | script: series( 99 | `nps copy.swagger`, 100 | `nps copy.public` 101 | ), 102 | hiddenFromHelp: true 103 | }, 104 | swagger: { 105 | script: copy( 106 | './src/api/swagger.json', 107 | './dist' 108 | ), 109 | hiddenFromHelp: true 110 | }, 111 | public: { 112 | script: copy( 113 | './src/public/*', 114 | './dist' 115 | ), 116 | hiddenFromHelp: true 117 | } 118 | }, 119 | /** 120 | * Database scripts 121 | */ 122 | db: { 123 | migrate: { 124 | script: series( 125 | 'nps banner.migrate', 126 | 'nps config', 127 | runFast('./node_modules/typeorm/cli.js migration:run') 128 | ), 129 | description: 'Migrates the database to newest version available' 130 | }, 131 | revert: { 132 | script: series( 133 | 'nps banner.revert', 134 | 'nps config', 135 | runFast('./node_modules/typeorm/cli.js migration:revert') 136 | ), 137 | description: 'Downgrades the database' 138 | }, 139 | seed: { 140 | script: series( 141 | 'nps banner.seed', 142 | 'nps config', 143 | runFast('./src/lib/seed/cli.ts') 144 | ), 145 | description: 'Seeds generated records into the database' 146 | }, 147 | drop: { 148 | script: runFast('./node_modules/typeorm/cli.js schema:drop'), 149 | description: 'Drops the schema of the database' 150 | } 151 | 152 | }, 153 | /** 154 | * These run various kinds of tests. Default is unit. 155 | */ 156 | test: { 157 | default: 'nps test.unit', 158 | unit: { 159 | default: { 160 | script: series( 161 | 'nps banner.testUnit', 162 | 'nps test.unit.pretest', 163 | 'nps test.unit.run' 164 | ), 165 | description: 'Runs the unit tests' 166 | }, 167 | pretest: { 168 | script: tslint(`./test/unit/**.ts`), 169 | hiddenFromHelp: true 170 | }, 171 | run: { 172 | script: 'cross-env NODE_ENV=test jest --testPathPattern=unit', 173 | hiddenFromHelp: true 174 | }, 175 | verbose: { 176 | script: 'nps "test --verbose"', 177 | hiddenFromHelp: true 178 | }, 179 | coverage: { 180 | script: 'nps "test --coverage"', 181 | hiddenFromHelp: true 182 | } 183 | }, 184 | integration: { 185 | default: { 186 | script: series( 187 | 'nps banner.testIntegration', 188 | 'nps test.integration.pretest', 189 | 'nps test.integration.run' 190 | ), 191 | description: 'Runs the integration tests' 192 | }, 193 | pretest: { 194 | script: tslint(`./test/integration/**.ts`), 195 | hiddenFromHelp: true 196 | }, 197 | run: { 198 | // -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. 199 | script: 'cross-env NODE_ENV=test jest --testPathPattern=integration -i', 200 | hiddenFromHelp: true 201 | }, 202 | verbose: { 203 | script: 'nps "test --verbose"', 204 | hiddenFromHelp: true 205 | }, 206 | coverage: { 207 | script: 'nps "test --coverage"', 208 | hiddenFromHelp: true 209 | } 210 | }, 211 | e2e: { 212 | default: { 213 | script: series( 214 | 'nps banner.testE2E', 215 | 'nps test.e2e.pretest', 216 | 'nps test.e2e.run' 217 | ), 218 | description: 'Runs the e2e tests' 219 | }, 220 | pretest: { 221 | script: tslint(`./test/e2e/**.ts`), 222 | hiddenFromHelp: true 223 | }, 224 | run: { 225 | // -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. 226 | script: 'cross-env NODE_ENV=test jest --testPathPattern=e2e -i', 227 | hiddenFromHelp: true 228 | }, 229 | verbose: { 230 | script: 'nps "test --verbose"', 231 | hiddenFromHelp: true 232 | }, 233 | coverage: { 234 | script: 'nps "test --coverage"', 235 | hiddenFromHelp: true 236 | } 237 | }, 238 | }, 239 | /** 240 | * This creates pretty banner to the terminal 241 | */ 242 | banner: { 243 | build: banner('build'), 244 | serve: banner('serve'), 245 | testUnit: banner('test.unit'), 246 | testIntegration: banner('test.integration'), 247 | testE2E: banner('test.e2e'), 248 | migrate: banner('migrate'), 249 | seed: banner('seed'), 250 | revert: banner('revert'), 251 | clean: banner('clean') 252 | } 253 | } 254 | }; 255 | 256 | function banner(name) { 257 | return { 258 | hiddenFromHelp: true, 259 | silent: true, 260 | description: `Shows ${name} banners to the console`, 261 | script: runFast(`./commands/banner.ts ${name}`), 262 | }; 263 | } 264 | 265 | function copy(source, target) { 266 | return `copyup ${source} ${target}`; 267 | } 268 | 269 | function run(path) { 270 | return `ts-node ${path}`; 271 | } 272 | 273 | function runFast(path) { 274 | return `ts-node --transpileOnly ${path}`; 275 | } 276 | 277 | function tslint(path) { 278 | return `tslint -c ./tslint.json ${path} --format stylish`; 279 | } 280 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-typescript-boilerplate", 3 | "version": "3.0.1", 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 | "@types/bluebird": "^3.5.18", 42 | "@types/body-parser": "^1.16.7", 43 | "@types/chalk": "^2.2.0", 44 | "@types/commander": "^2.11.0", 45 | "@types/cors": "^2.8.1", 46 | "@types/dotenv": "^4.0.2", 47 | "@types/express": "^4.0.39", 48 | "@types/faker": "^4.1.2", 49 | "@types/helmet": "^0.0.37", 50 | "@types/lodash": "^4.14.80", 51 | "@types/morgan": "^1.7.35", 52 | "@types/reflect-metadata": "0.1.0", 53 | "@types/request": "^2.0.8", 54 | "@types/serve-favicon": "^2.2.29", 55 | "@types/supertest": "^2.0.4", 56 | "@types/uuid": "^3.4.3", 57 | "@types/winston": "^2.3.7", 58 | "body-parser": "^1.18.2", 59 | "chalk": "^2.3.0", 60 | "class-validator": "^0.8.5", 61 | "commander": "^2.11.0", 62 | "compression": "^1.7.1", 63 | "copyfiles": "^2.0.0", 64 | "cors": "^2.8.4", 65 | "dataloader": "^1.3.0", 66 | "dotenv": "^5.0.1", 67 | "event-dispatch": "^0.4.1", 68 | "express": "^4.16.2", 69 | "express-basic-auth": "^1.1.3", 70 | "express-graphql": "^0.6.11", 71 | "express-status-monitor": "^1.0.1", 72 | "faker": "^4.1.0", 73 | "figlet": "^1.2.0", 74 | "glob": "^7.1.2", 75 | "graphql": "^0.13.2", 76 | "helmet": "^3.9.0", 77 | "jsonfile": "^4.0.0", 78 | "lodash": "^4.17.4", 79 | "microframework-w3tec": "^0.6.3", 80 | "morgan": "^1.9.0", 81 | "mysql": "^2.15.0", 82 | "nodemon": "^1.12.1", 83 | "nps": "^5.7.1", 84 | "nps-utils": "^1.5.0", 85 | "path": "^0.12.7", 86 | "pg": "^7.4.3", 87 | "reflect-metadata": "^0.1.10", 88 | "request": "^2.83.0", 89 | "routing-controllers": "^0.7.6", 90 | "serve-favicon": "^2.4.5", 91 | "supertest": "^3.0.0", 92 | "swagger-ui-express": "^3.0.8", 93 | "ts-node": "^6.0.0", 94 | "tslint": "^5.8.0", 95 | "typedi": "^0.7.2", 96 | "typeorm": "^0.2.1", 97 | "typeorm-typedi-extensions": "^0.2.1", 98 | "typescript": "2.8.3", 99 | "uuid": "^3.1.0", 100 | "winston": "^2.4.0" 101 | }, 102 | "jest": { 103 | "transform": { 104 | ".(ts|tsx)": "/test/preprocessor.js" 105 | }, 106 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 107 | "moduleFileExtensions": [ 108 | "ts", 109 | "tsx", 110 | "js", 111 | "json" 112 | ], 113 | "testEnvironment": "node", 114 | "setupTestFrameworkScriptFile": "./test/unit/lib/setup.ts" 115 | }, 116 | "license": "MIT", 117 | "devDependencies": { 118 | "@types/jest": "^22.2.3", 119 | "@types/nock": "^9.1.3", 120 | "cross-env": "^5.1.1", 121 | "jest": "^22.4.3", 122 | "mock-express-request": "^0.2.0", 123 | "mock-express-response": "^0.2.1", 124 | "nock": "^9.1.4", 125 | "sqlite3": "^4.0.0", 126 | "ts-jest": "^22.4.4" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/api/controllers/PetController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put 3 | } from 'routing-controllers'; 4 | 5 | import { PetNotFoundError } from '../errors/PetNotFoundError'; 6 | import { Pet } from '../models/Pet'; 7 | import { PetService } from '../services/PetService'; 8 | 9 | @Authorized() 10 | @JsonController('/pets') 11 | export class PetController { 12 | 13 | constructor( 14 | private petService: PetService 15 | ) { } 16 | 17 | @Get() 18 | public find(): Promise { 19 | return this.petService.find(); 20 | } 21 | 22 | @Get('/:id') 23 | @OnUndefined(PetNotFoundError) 24 | public one( @Param('id') id: string): Promise { 25 | return this.petService.findOne(id); 26 | } 27 | 28 | @Post() 29 | public create( @Body() pet: Pet): Promise { 30 | return this.petService.create(pet); 31 | } 32 | 33 | @Put('/:id') 34 | public update( @Param('id') id: string, @Body() pet: Pet): Promise { 35 | return this.petService.update(id, pet); 36 | } 37 | 38 | @Delete('/:id') 39 | public delete( @Param('id') id: string): Promise { 40 | return this.petService.delete(id); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/api/controllers/UserController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Authorized, Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Param, Post, Put 3 | } from 'routing-controllers'; 4 | 5 | import { UserNotFoundError } from '../errors/UserNotFoundError'; 6 | import { User } from '../models/User'; 7 | import { UserService } from '../services/UserService'; 8 | 9 | @Authorized() 10 | @JsonController('/users') 11 | export class UserController { 12 | 13 | constructor( 14 | private userService: UserService 15 | ) { } 16 | 17 | @Get() 18 | public find( @CurrentUser() user?: User): Promise { 19 | return this.userService.find(); 20 | } 21 | 22 | @Get('/:id') 23 | @OnUndefined(UserNotFoundError) 24 | public one( @Param('id') id: string): Promise { 25 | return this.userService.findOne(id); 26 | } 27 | 28 | @Post() 29 | public create( @Body() user: User): Promise { 30 | return this.userService.create(user); 31 | } 32 | 33 | @Put('/:id') 34 | public update( @Param('id') id: string, @Body() user: User): Promise { 35 | return this.userService.update(id, user); 36 | } 37 | 38 | @Delete('/:id') 39 | public delete( @Param('id') id: string): Promise { 40 | return this.userService.delete(id); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/api/controllers/requests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeselaGen/typeorm-backend/2456e909f3a4b44d9be65e0a05d0eec1a23705b9/src/api/controllers/requests/.gitkeep -------------------------------------------------------------------------------- /src/api/controllers/responses/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeselaGen/typeorm-backend/2456e909f3a4b44d9be65e0a05d0eec1a23705b9/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/TeselaGen/typeorm-backend/2456e909f3a4b44d9be65e0a05d0eec1a23705b9/src/api/interceptors/.gitkeep -------------------------------------------------------------------------------- /src/api/middlewares/CompressionMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as 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 * as 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 * as 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, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | import { User } from './User'; 5 | 6 | @Entity() 7 | export class Pet { 8 | 9 | @PrimaryGeneratedColumn('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: number; 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 { IsNotEmpty } from 'class-validator'; 2 | import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | import { Pet } from './Pet'; 5 | 6 | @Entity() 7 | export class User { 8 | 9 | @PrimaryGeneratedColumn('uuid') 10 | public id: string; 11 | 12 | @IsNotEmpty() 13 | @Column({ name: 'first_name' }) 14 | public firstName: string; 15 | 16 | @IsNotEmpty() 17 | @Column({ name: 'last_name' }) 18 | public lastName: string; 19 | 20 | @Column({ name: 'gender' }) 21 | public gender: string; 22 | 23 | @IsNotEmpty() 24 | @Column() 25 | public email: string; 26 | 27 | @OneToMany(type => Pet, pet => pet.user) 28 | public pets: Pet[]; 29 | 30 | public toString(): string { 31 | return `${this.firstName} ${this.lastName} (${this.email})`; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/api/mutations/CreatePetMutation.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from 'class-transformer'; 2 | import { GraphQLFieldConfig, GraphQLInt, GraphQLNonNull, GraphQLString } from 'graphql'; 3 | 4 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 5 | import { GraphQLContext, Mutation } from '../../lib/graphql'; 6 | import { AbstractGraphQLMutation } from '../../lib/graphql/AbstractGraphQLMutation'; 7 | import { Pet } from '../models/Pet'; 8 | import { PetService } from '../services/PetService'; 9 | import { PetType } from '../types/PetType'; 10 | 11 | interface CreatePetMutationArguments { 12 | name: string; 13 | age: number; 14 | } 15 | 16 | @Mutation() 17 | export class CreatePetMutation extends AbstractGraphQLMutation, Pet, CreatePetMutationArguments> implements GraphQLFieldConfig { 18 | public type = PetType; 19 | public args = { 20 | name: { type: new GraphQLNonNull(GraphQLString) }, 21 | age: { type: new GraphQLNonNull(GraphQLInt) }, 22 | }; 23 | 24 | constructor( 25 | private petService: PetService, 26 | @Logger(__filename) private log: LoggerInterface 27 | ) { 28 | super(); 29 | } 30 | 31 | public async run(root: any, args: CreatePetMutationArguments, context: GraphQLContext): Promise { 32 | const pet = await this.petService.create(plainToClass(Pet, args)); 33 | this.log.info('Successfully created a new pet'); 34 | return pet; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/api/queries/GetPetsQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfig, GraphQLList } from 'graphql'; 2 | 3 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 4 | import { AbstractGraphQLQuery, GraphQLContext, Query } from '../../lib/graphql'; 5 | import { Pet } from '../models/Pet'; 6 | import { PetService } from '../services/PetService'; 7 | import { PetType } from '../types/PetType'; 8 | 9 | @Query() 10 | export class GetPetsQuery extends AbstractGraphQLQuery, Pet[], any> implements GraphQLFieldConfig { 11 | public type = new GraphQLList(PetType); 12 | public allow = []; 13 | public args = {}; 14 | 15 | constructor( 16 | private petService: PetService, 17 | @Logger(__filename) private log: LoggerInterface 18 | ) { 19 | super(); 20 | } 21 | 22 | public async run(root: any, args: any, context: GraphQLContext): Promise { 23 | const pets = await this.petService.find(); 24 | this.log.info(`Found ${pets.length} pets`); 25 | return pets; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/api/queries/userQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfig, GraphQLString } from 'graphql'; 2 | 3 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 4 | import { AbstractGraphQLQuery, GraphQLContext, Query } from '../../lib/graphql'; 5 | import { User } from '../models/User'; 6 | import { UserService } from '../services/UserService'; 7 | import { UserType } from '../types/UserType'; 8 | 9 | @Query() 10 | export class userQuery extends AbstractGraphQLQuery, User, any> implements GraphQLFieldConfig { 11 | public type = UserType; 12 | public allow = {}; 13 | public args = { 14 | id: { 15 | type: GraphQLString, 16 | description: "id user", 17 | required: true 18 | } 19 | }; 20 | 21 | constructor( 22 | private userService: UserService, 23 | @Logger(__filename) private log: LoggerInterface 24 | ) { 25 | super(); 26 | } 27 | 28 | public async run(root: any, args: any, context: GraphQLContext): Promise { 29 | const user = await this.userService.findOne(args.id); 30 | this.log.info(`Found user`,user); 31 | return user; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/api/queries/usersQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfig } from "graphql"; 2 | import { Logger, LoggerInterface } from "../../decorators/Logger"; 3 | import { AbstractGraphQLQuery, GraphQLContext, Query } from "../../lib/graphql"; 4 | // import { User } from '../models/User'; 5 | import { UserService } from "../services/UserService"; 6 | import { userCursorResult } from "../resultTypes/userCursorResult"; 7 | import { UserCustorResultModel } from "../resultModels/UserCustorResult"; 8 | import cursorFilterType from "../types/cursorFilterType"; 9 | 10 | @Query() 11 | export class usersQuery 12 | extends AbstractGraphQLQuery< 13 | GraphQLContext, 14 | UserCustorResultModel, 15 | any 16 | > 17 | implements GraphQLFieldConfig { 18 | public type = userCursorResult; 19 | public allow = []; 20 | public args = { 21 | filter: { 22 | type: cursorFilterType, 23 | description: "filtro de users" 24 | } 25 | }; 26 | 27 | constructor( 28 | private userService: UserService, 29 | @Logger(__filename) private log: LoggerInterface 30 | ) { 31 | super(); 32 | } 33 | 34 | public async run( 35 | root: any, 36 | args: any, 37 | context: GraphQLContext 38 | ): Promise { 39 | const users = await this.userService.find(); 40 | this.log.info(`Found ${users.length} users`); 41 | return { 42 | totalResults: 1, 43 | results: [] 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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/resultModels/UserCustorResult.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'typeorm'; 2 | 3 | @Entity() 4 | export class UserCustorResultModel { 5 | public totalResults: number; 6 | public results: any[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/api/resultTypes/userCursorResult.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLFieldConfigMap, GraphQLInt, GraphQLList, GraphQLObjectType 3 | } from 'graphql'; 4 | import { UserType } from '../types/UserType' 5 | 6 | 7 | const UserFields: GraphQLFieldConfigMap = { 8 | totalResults: { 9 | type: GraphQLInt, 10 | description: 'contaos los resultaos', 11 | }, 12 | results: { 13 | type: GraphQLList(UserType), 14 | description: 'los resultaos de verda', 15 | } 16 | }; 17 | 18 | export const userCursorResult = new GraphQLObjectType({ 19 | name: 'userCursorResult', 20 | description: 'Resultaos de usuarios.', 21 | fields: () => ({ ...UserFields}) 22 | }); -------------------------------------------------------------------------------- /src/api/services/PetService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { OrmRepository } from 'typeorm-typedi-extensions'; 3 | 4 | import { EventDispatcher, EventDispatcherInterface } from '../../decorators/EventDispatcher'; 5 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 6 | import { Pet } from '../models/Pet'; 7 | import { User } from '../models/User'; 8 | import { PetRepository } from '../repositories/PetRepository'; 9 | import { events } from '../subscribers/events'; 10 | 11 | @Service() 12 | export class PetService { 13 | 14 | constructor( 15 | @OrmRepository() private petRepository: PetRepository, 16 | @EventDispatcher() private eventDispatcher: EventDispatcherInterface, 17 | @Logger(__filename) private log: LoggerInterface 18 | ) { } 19 | 20 | public find(): Promise { 21 | this.log.info('Find all pets'); 22 | return this.petRepository.find(); 23 | } 24 | 25 | public findByUser(user: User): Promise { 26 | this.log.info('Find all pets of the user', user.toString()); 27 | return this.petRepository.find({ 28 | where: { 29 | userId: user.id, 30 | }, 31 | }); 32 | } 33 | 34 | public findOne(id: string): Promise { 35 | this.log.info('Find all pets'); 36 | return this.petRepository.findOne({ id }); 37 | } 38 | 39 | public async create(pet: Pet): Promise { 40 | this.log.info('Create a new pet => ', pet.toString()); 41 | const newPet = await this.petRepository.save(pet); 42 | this.eventDispatcher.dispatch(events.pet.created, newPet); 43 | return newPet; 44 | } 45 | 46 | public update(id: string, pet: Pet): Promise { 47 | this.log.info('Update a pet'); 48 | pet.id = id; 49 | return this.petRepository.save(pet); 50 | } 51 | 52 | public async delete(id: string): Promise { 53 | this.log.info('Delete a pet'); 54 | await this.petRepository.delete(id); 55 | return; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/api/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { OrmRepository } from 'typeorm-typedi-extensions'; 3 | 4 | import { EventDispatcher, EventDispatcherInterface } from '../../decorators/EventDispatcher'; 5 | import { Logger, LoggerInterface } from '../../decorators/Logger'; 6 | import { User } from '../models/User'; 7 | import { UserRepository } from '../repositories/UserRepository'; 8 | import { events } from '../subscribers/events'; 9 | 10 | @Service() 11 | export class UserService { 12 | 13 | constructor( 14 | @OrmRepository() private userRepository: UserRepository, 15 | @EventDispatcher() private eventDispatcher: EventDispatcherInterface, 16 | @Logger(__filename) private log: LoggerInterface 17 | ) { } 18 | 19 | public find(): Promise { 20 | this.log.info('Find all users'); 21 | return this.userRepository.find({ relations: ['pets'] }); 22 | } 23 | 24 | public findOne(id: string): Promise { 25 | this.log.info('Find all users'); 26 | return this.userRepository.findOne({ id }); 27 | } 28 | 29 | public async create(user: User): Promise { 30 | this.log.info('Create a new user => ', user.toString()); 31 | const newUser = await this.userRepository.save(user); 32 | this.eventDispatcher.dispatch(events.user.created, newUser); 33 | return newUser; 34 | } 35 | 36 | public update(id: string, user: User): Promise { 37 | this.log.info('Update a user'); 38 | user.id = id; 39 | return this.userRepository.save(user); 40 | } 41 | 42 | public async delete(id: string): Promise { 43 | this.log.info('Delete a user'); 44 | await this.userRepository.delete(id); 45 | return; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /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/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "host": "", 4 | "info": { 5 | "title": "", 6 | "description": "", 7 | "version": "" 8 | }, 9 | "basePath": "", 10 | "consumes": [ 11 | "application/json" 12 | ], 13 | "produces": [ 14 | "application/json" 15 | ], 16 | "schemes": [], 17 | "securityDefinitions": { 18 | "JWT": { 19 | "type": "apiKey", 20 | "in": "header", 21 | "name": "Authorization" 22 | } 23 | }, 24 | "tags": [ 25 | { 26 | "name": "Info" 27 | }, 28 | { 29 | "name": "User" 30 | }, 31 | { 32 | "name": "Pet" 33 | } 34 | ], 35 | "paths": { 36 | "/": { 37 | "get": { 38 | "tags": [ 39 | "Info" 40 | ], 41 | "summary": "Show API information", 42 | "description": "This is a public route", 43 | "operationId": "showApiInfo", 44 | "responses": { 45 | "200": { 46 | "description": "ok" 47 | } 48 | } 49 | } 50 | }, 51 | "/users": { 52 | "get": { 53 | "tags": [ 54 | "User" 55 | ], 56 | "summary": "List all users", 57 | "description": "Returns all users", 58 | "operationId": "FindAllUsers", 59 | "security": [ 60 | { 61 | "JWT": [] 62 | } 63 | ], 64 | "responses": { 65 | "200": { 66 | "description": "ok", 67 | "schema": { 68 | "type": "object", 69 | "properties": { 70 | "success": { 71 | "type": "boolean" 72 | }, 73 | "message": { 74 | "type": "string" 75 | }, 76 | "data": { 77 | "type": "array", 78 | "items": { 79 | "$ref": "#/definitions/User" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | }, 87 | "post": { 88 | "tags": [ 89 | "User" 90 | ], 91 | "summary": "Create new user", 92 | "description": "", 93 | "operationId": "CreateUser", 94 | "security": [ 95 | { 96 | "JWT": [] 97 | } 98 | ], 99 | "parameters": [ 100 | { 101 | "in": "body", 102 | "name": "body", 103 | "description": "User object that needs to be added to the database", 104 | "required": true, 105 | "schema": { 106 | "$ref": "#/definitions/NewUser" 107 | } 108 | } 109 | ], 110 | "responses": { 111 | "201": { 112 | "description": "created", 113 | "schema": { 114 | "type": "object", 115 | "properties": { 116 | "success": { 117 | "type": "boolean" 118 | }, 119 | "message": { 120 | "type": "string" 121 | }, 122 | "data": { 123 | "$ref": "#/definitions/User" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | "/users/{id}": { 132 | "get": { 133 | "tags": [ 134 | "User" 135 | ], 136 | "summary": "Get user", 137 | "description": "Returns the given user", 138 | "operationId": "FindUser", 139 | "security": [ 140 | { 141 | "JWT": [] 142 | } 143 | ], 144 | "parameters": [ 145 | { 146 | "name": "id", 147 | "in": "path", 148 | "description": "ID of user to return", 149 | "required": true, 150 | "type": "integer", 151 | "format": "int64" 152 | } 153 | ], 154 | "responses": { 155 | "200": { 156 | "description": "ok", 157 | "schema": { 158 | "type": "object", 159 | "properties": { 160 | "success": { 161 | "type": "boolean" 162 | }, 163 | "message": { 164 | "type": "string" 165 | }, 166 | "data": { 167 | "$ref": "#/definitions/User" 168 | } 169 | } 170 | } 171 | } 172 | } 173 | }, 174 | "put": { 175 | "tags": [ 176 | "User" 177 | ], 178 | "summary": "Update user", 179 | "description": "Updates the given user", 180 | "operationId": "UpdateUser", 181 | "security": [ 182 | { 183 | "JWT": [] 184 | } 185 | ], 186 | "parameters": [ 187 | { 188 | "name": "id", 189 | "in": "path", 190 | "description": "ID of user to update", 191 | "required": true, 192 | "type": "integer", 193 | "format": "int64" 194 | }, 195 | { 196 | "in": "body", 197 | "name": "body", 198 | "description": "User object that needs to be added to the database", 199 | "required": true, 200 | "schema": { 201 | "$ref": "#/definitions/NewUser" 202 | } 203 | } 204 | ], 205 | "responses": { 206 | "200": { 207 | "description": "ok", 208 | "schema": { 209 | "type": "object", 210 | "properties": { 211 | "success": { 212 | "type": "boolean" 213 | }, 214 | "message": { 215 | "type": "string" 216 | }, 217 | "data": { 218 | "$ref": "#/definitions/User" 219 | } 220 | } 221 | } 222 | } 223 | } 224 | }, 225 | "delete": { 226 | "tags": [ 227 | "User" 228 | ], 229 | "summary": "Delete user", 230 | "description": "Removes the given user", 231 | "operationId": "DeleteUser", 232 | "security": [ 233 | { 234 | "JWT": [] 235 | } 236 | ], 237 | "parameters": [ 238 | { 239 | "name": "id", 240 | "in": "path", 241 | "description": "ID of user to delete", 242 | "required": true, 243 | "type": "integer", 244 | "format": "int64" 245 | } 246 | ], 247 | "responses": { 248 | "200": { 249 | "description": "ok", 250 | "schema": { 251 | "type": "object", 252 | "properties": { 253 | "success": { 254 | "type": "boolean" 255 | }, 256 | "message": { 257 | "type": "string" 258 | } 259 | } 260 | } 261 | } 262 | } 263 | } 264 | }, 265 | "/pets": { 266 | "get": { 267 | "tags": [ 268 | "Pet" 269 | ], 270 | "summary": "List all pets", 271 | "description": "Returns all pets", 272 | "operationId": "FindAllPets", 273 | "security": [ 274 | { 275 | "JWT": [] 276 | } 277 | ], 278 | "responses": { 279 | "200": { 280 | "description": "ok", 281 | "schema": { 282 | "type": "object", 283 | "properties": { 284 | "success": { 285 | "type": "boolean" 286 | }, 287 | "message": { 288 | "type": "string" 289 | }, 290 | "data": { 291 | "type": "array", 292 | "items": { 293 | "$ref": "#/definitions/Pet" 294 | } 295 | } 296 | } 297 | } 298 | } 299 | } 300 | }, 301 | "post": { 302 | "tags": [ 303 | "Pet" 304 | ], 305 | "summary": "Create new pet", 306 | "description": "", 307 | "operationId": "CreatePet", 308 | "security": [ 309 | { 310 | "JWT": [] 311 | } 312 | ], 313 | "parameters": [ 314 | { 315 | "in": "body", 316 | "name": "body", 317 | "description": "User object that needs to be added to the database", 318 | "required": true, 319 | "schema": { 320 | "$ref": "#/definitions/NewPet" 321 | } 322 | } 323 | ], 324 | "responses": { 325 | "201": { 326 | "description": "created", 327 | "schema": { 328 | "type": "object", 329 | "properties": { 330 | "success": { 331 | "type": "boolean" 332 | }, 333 | "message": { 334 | "type": "string" 335 | }, 336 | "data": { 337 | "$ref": "#/definitions/Pet" 338 | } 339 | } 340 | } 341 | } 342 | } 343 | } 344 | }, 345 | "/pets/{id}": { 346 | "get": { 347 | "tags": [ 348 | "Pet" 349 | ], 350 | "summary": "Get pet", 351 | "description": "Returns the given pet", 352 | "operationId": "FindPet", 353 | "security": [ 354 | { 355 | "JWT": [] 356 | } 357 | ], 358 | "parameters": [ 359 | { 360 | "name": "id", 361 | "in": "path", 362 | "description": "ID of pet to return", 363 | "required": true, 364 | "type": "integer", 365 | "format": "int64" 366 | } 367 | ], 368 | "responses": { 369 | "200": { 370 | "description": "ok", 371 | "schema": { 372 | "type": "object", 373 | "properties": { 374 | "success": { 375 | "type": "boolean" 376 | }, 377 | "message": { 378 | "type": "string" 379 | }, 380 | "data": { 381 | "$ref": "#/definitions/Pet" 382 | } 383 | } 384 | } 385 | } 386 | } 387 | }, 388 | "put": { 389 | "tags": [ 390 | "Pet" 391 | ], 392 | "summary": "Update pet", 393 | "description": "Updates the given pet", 394 | "operationId": "UpdatePet", 395 | "security": [ 396 | { 397 | "JWT": [] 398 | } 399 | ], 400 | "parameters": [ 401 | { 402 | "name": "id", 403 | "in": "path", 404 | "description": "ID of pet to update", 405 | "required": true, 406 | "type": "integer", 407 | "format": "int64" 408 | }, 409 | { 410 | "in": "body", 411 | "name": "body", 412 | "description": "Pet object that needs to be added to the database", 413 | "required": true, 414 | "schema": { 415 | "$ref": "#/definitions/NewPet" 416 | } 417 | } 418 | ], 419 | "responses": { 420 | "200": { 421 | "description": "ok", 422 | "schema": { 423 | "type": "object", 424 | "properties": { 425 | "success": { 426 | "type": "boolean" 427 | }, 428 | "message": { 429 | "type": "string" 430 | }, 431 | "data": { 432 | "$ref": "#/definitions/Pet" 433 | } 434 | } 435 | } 436 | } 437 | } 438 | }, 439 | "delete": { 440 | "tags": [ 441 | "Pet" 442 | ], 443 | "summary": "Delete pet", 444 | "description": "Removes the given pet", 445 | "operationId": "DeletePet", 446 | "security": [ 447 | { 448 | "JWT": [] 449 | } 450 | ], 451 | "parameters": [ 452 | { 453 | "name": "id", 454 | "in": "path", 455 | "description": "ID of pet to delete", 456 | "required": true, 457 | "type": "integer", 458 | "format": "int64" 459 | } 460 | ], 461 | "responses": { 462 | "200": { 463 | "description": "ok", 464 | "schema": { 465 | "type": "object", 466 | "properties": { 467 | "success": { 468 | "type": "boolean" 469 | }, 470 | "message": { 471 | "type": "string" 472 | } 473 | } 474 | } 475 | } 476 | } 477 | } 478 | } 479 | }, 480 | "definitions": { 481 | "NewUser": { 482 | "type": "object", 483 | "properties": { 484 | "firstName": { 485 | "type": "string" 486 | }, 487 | "lastName": { 488 | "type": "string" 489 | }, 490 | "email": { 491 | "type": "string" 492 | }, 493 | "gender": { 494 | "type": "string" 495 | }, 496 | "picture": { 497 | "type": "string" 498 | } 499 | } 500 | }, 501 | "NewPet": { 502 | "type": "object", 503 | "properties": { 504 | "name": { 505 | "type": "string" 506 | }, 507 | "age": { 508 | "type": "integer", 509 | "format": "int64" 510 | }, 511 | "userId": { 512 | "type": "integer", 513 | "format": "int64" 514 | } 515 | } 516 | }, 517 | "User": { 518 | "title": "User", 519 | "allOf": [ 520 | { 521 | "$ref": "#/definitions/NewUser" 522 | }, 523 | { 524 | "type": "object", 525 | "properties": { 526 | "id": { 527 | "type": "integer", 528 | "format": "int64" 529 | }, 530 | "updatedAt": { 531 | "type": "string", 532 | "format": "date-time" 533 | }, 534 | "createdAt": { 535 | "type": "string", 536 | "format": "date-time" 537 | } 538 | } 539 | } 540 | ] 541 | }, 542 | "Pet": { 543 | "title": "Pet", 544 | "allOf": [ 545 | { 546 | "type": "object", 547 | "properties": { 548 | "id": { 549 | "type": "integer", 550 | "format": "int64" 551 | }, 552 | "name": { 553 | "type": "string" 554 | }, 555 | "age": { 556 | "type": "integer", 557 | "format": "int64" 558 | }, 559 | "user": { 560 | "$ref": "#/definitions/User" 561 | }, 562 | "updatedAt": { 563 | "type": "string", 564 | "format": "date-time" 565 | }, 566 | "createdAt": { 567 | "type": "string", 568 | "format": "date-time" 569 | } 570 | } 571 | } 572 | ] 573 | } 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /src/api/types/PetType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLFieldConfigMap, GraphQLID, GraphQLInt, GraphQLObjectType, GraphQLString 3 | } from 'graphql'; 4 | 5 | import { GraphQLContext } from '../../lib/graphql'; 6 | import { Pet } from '../models/Pet'; 7 | import { OwnerType } from './UserType'; 8 | 9 | const PetFields: GraphQLFieldConfigMap = { 10 | id: { 11 | type: GraphQLID, 12 | description: 'The ID', 13 | }, 14 | name: { 15 | type: GraphQLString, 16 | description: 'The name of the pet.', 17 | }, 18 | age: { 19 | type: GraphQLInt, 20 | description: 'The age of the pet in years.', 21 | }, 22 | }; 23 | 24 | export const PetOfUserType = new GraphQLObjectType({ 25 | name: 'PetOfUser', 26 | description: 'A users pet', 27 | fields: () => ({ ...PetFields, ...{} }), 28 | }); 29 | 30 | export const PetType = new GraphQLObjectType({ 31 | name: 'Pet', 32 | description: 'A single pet.', 33 | fields: () => ({ ...PetFields, ...{ 34 | owner: { 35 | type: OwnerType, 36 | description: 'The owner of the pet', 37 | resolve: (pet: Pet, args: any, context: GraphQLContext) => 38 | context.dataLoaders.user.load(pet.userId), 39 | }, 40 | } }), 41 | }); 42 | -------------------------------------------------------------------------------- /src/api/types/UserType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLFieldConfigMap, GraphQLID, GraphQLList, GraphQLObjectType, GraphQLString 3 | } from 'graphql'; 4 | 5 | import { GraphQLContext } from '../../lib/graphql'; 6 | import { User } from '../models/User'; 7 | import { PetOfUserType } from './PetType'; 8 | 9 | const UserFields: GraphQLFieldConfigMap = { 10 | id: { 11 | type: GraphQLID, 12 | description: 'The ID', 13 | }, 14 | firstName: { 15 | type: GraphQLString, 16 | description: 'The first name of the user.', 17 | }, 18 | lastName: { 19 | type: GraphQLString, 20 | description: 'The last name of the user.', 21 | }, 22 | email: { 23 | type: GraphQLString, 24 | description: 'The email of this user.', 25 | }, 26 | gender: { 27 | type: GraphQLString, 28 | description: 'Gender of the user.', 29 | }, 30 | }; 31 | 32 | export const UserType = new GraphQLObjectType({ 33 | name: 'User', 34 | description: 'A single user.', 35 | fields: () => ({ ...UserFields, ...{ 36 | pets: { 37 | type: new GraphQLList(PetOfUserType), 38 | description: 'The pets of a user', 39 | resolve: async (user: User, args: any, context: GraphQLContext) => 40 | // We use data-loaders to save db queries 41 | context.dataLoaders.petsByUserIds.load(user.id), 42 | // This would be the case with a normal service, but not very fast 43 | // context.container.get(PetService).findByUser(user), 44 | }, 45 | } }), 46 | }); 47 | 48 | export const OwnerType = new GraphQLObjectType({ 49 | name: 'Owner', 50 | description: 'The owner of a pet', 51 | fields: () => ({ ...UserFields, ...{} }), 52 | }); 53 | -------------------------------------------------------------------------------- /src/api/types/cursorFilterType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | import { Kind } from 'graphql/language'; 3 | 4 | function identity(value) { 5 | return value; 6 | } 7 | 8 | function parseLiteral(ast, variables) { 9 | switch (ast.kind) { 10 | case Kind.STRING: 11 | case Kind.BOOLEAN: 12 | return ast.value; 13 | case Kind.INT: 14 | case Kind.FLOAT: 15 | return parseFloat(ast.value); 16 | case Kind.OBJECT: { 17 | const value = Object.create(null); 18 | ast.fields.forEach(field => { 19 | value[field.name.value] = parseLiteral(field.value, variables); 20 | }); 21 | 22 | return value; 23 | } 24 | case Kind.LIST: 25 | return ast.values.map(n => parseLiteral(n, variables)); 26 | case Kind.NULL: 27 | return null; 28 | case Kind.VARIABLE: { 29 | const name = ast.name.value; 30 | return variables ? variables[name] : undefined; 31 | } 32 | default: 33 | return undefined; 34 | } 35 | } 36 | 37 | export default new GraphQLScalarType({ 38 | name: 'JSON', 39 | description: 40 | 'The `JSON` scalar type represents JSON values as specified by ' + 41 | '[ECMA-404](http://www.ecma-international.org/' + 42 | 'publications/files/ECMA-ST/ECMA-404.pdf).', 43 | serialize: identity, 44 | parseValue: identity, 45 | parseLiteral, 46 | }); 47 | -------------------------------------------------------------------------------- /src/api/validators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeselaGen/typeorm-backend/2456e909f3a4b44d9be65e0a05d0eec1a23705b9/src/api/validators/.gitkeep -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapMicroframework } from 'microframework-w3tec'; 2 | import 'reflect-metadata'; 3 | 4 | import { banner } from './lib/banner'; 5 | import { Logger } from './lib/logger'; 6 | import { eventDispatchLoader } from './loaders/eventDispatchLoader'; 7 | import { expressLoader } from './loaders/expressLoader'; 8 | import { graphqlLoader } from './loaders/graphqlLoader'; 9 | import { homeLoader } from './loaders/homeLoader'; 10 | import { iocLoader } from './loaders/iocLoader'; 11 | import { monitorLoader } from './loaders/monitorLoader'; 12 | import { publicLoader } from './loaders/publicLoader'; 13 | import { swaggerLoader } from './loaders/swaggerLoader'; 14 | import { typeormLoader } from './loaders/typeormLoader'; 15 | import { winstonLoader } from './loaders/winstonLoader'; 16 | 17 | /** 18 | * EXPRESS TYPESCRIPT BOILERPLATE 19 | * ---------------------------------------- 20 | * 21 | * This is a boilerplate for Node.js Application written in TypeScript. 22 | * The basic layer of this app is express. For further information visit 23 | * the 'README.md' file. 24 | */ 25 | const log = new Logger(__filename); 26 | 27 | bootstrapMicroframework({ 28 | /** 29 | * Loader is a place where you can configure all your modules during microframework 30 | * bootstrap process. All loaders are executed one by one in a sequential order. 31 | */ 32 | loaders: [ 33 | winstonLoader, 34 | iocLoader, 35 | eventDispatchLoader, 36 | typeormLoader, 37 | expressLoader, 38 | swaggerLoader, 39 | monitorLoader, 40 | homeLoader, 41 | publicLoader, 42 | graphqlLoader, 43 | ], 44 | }) 45 | .then(() => banner(log)) 46 | .catch(error => log.error('Application is crashed: ' + error)); 47 | -------------------------------------------------------------------------------- /src/auth/AuthService.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as request from 'request'; 3 | import { Service } from 'typedi'; 4 | 5 | import { Logger, LoggerInterface } from '../decorators/Logger'; 6 | import { env } from '../env'; 7 | import { TokenInfoInterface } from './TokenInfoInterface'; 8 | 9 | @Service() 10 | export class AuthService { 11 | 12 | private httpRequest: typeof request; 13 | 14 | constructor( 15 | @Logger(__filename) private log: LoggerInterface 16 | ) { 17 | this.httpRequest = request; 18 | } 19 | 20 | public parseTokenFromRequest(req: express.Request): string | undefined { 21 | const authorization = req.header('authorization'); 22 | 23 | // Retrieve the token form the Authorization header 24 | if (authorization && authorization.split(' ')[0] === 'Bearer') { 25 | this.log.info('Token provided by the client'); 26 | return authorization.split(' ')[1]; 27 | } 28 | 29 | this.log.info('No Token provided by the client'); 30 | return undefined; 31 | } 32 | 33 | public getTokenInfo(token: string): Promise { 34 | return new Promise((resolve, reject) => { 35 | this.httpRequest({ 36 | method: 'POST', 37 | url: env.auth.route, 38 | form: { 39 | id_token: token, 40 | }, 41 | }, (error: any, response: request.RequestResponse, body: any) => { 42 | // Verify if the requests was successful and append user 43 | // information to our extended express request object 44 | if (!error) { 45 | if (response.statusCode === 200) { 46 | const tokeninfo = JSON.parse(body); 47 | return resolve(tokeninfo); 48 | } 49 | return reject(body); 50 | } 51 | return reject(error); 52 | }); 53 | }); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/auth/TokenInfoInterface.ts: -------------------------------------------------------------------------------- 1 | export interface TokenInfoInterface { 2 | user_id: string; 3 | } 4 | -------------------------------------------------------------------------------- /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 | // demo code: 19 | const token = authService.parseTokenFromRequest(action.request); 20 | 21 | if (token === undefined) { 22 | log.warn('No token given'); 23 | return false; 24 | } 25 | 26 | // Request user info at auth0 with the provided token 27 | try { 28 | action.request.tokeninfo = await authService.getTokenInfo(token); 29 | log.info('Successfully checked token'); 30 | return true; 31 | } catch (e) { 32 | log.warn(e); 33 | return false; 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/auth/currentUserChecker.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'routing-controllers'; 2 | import { Connection } from 'typeorm'; 3 | 4 | import { User } from '../api/models/User'; 5 | import { Logger } from '../lib/logger'; 6 | import { TokenInfoInterface } from './TokenInfoInterface'; 7 | 8 | export function currentUserChecker(connection: Connection): (action: Action) => Promise { 9 | const log = new Logger(__filename); 10 | 11 | return async function innerCurrentUserChecker(action: Action): Promise { 12 | // here you can use request/response objects from action 13 | // you need to provide a user object that will be injected in controller actions 14 | // demo code: 15 | const tokeninfo: TokenInfoInterface = action.request.tokeninfo; 16 | const em = connection.createEntityManager(); 17 | const user = await em.findOne(User, { 18 | where: { 19 | email: tokeninfo.user_id.replace('auth0|', ''), 20 | }, 21 | }); 22 | if (user) { 23 | log.info('Current user is ', user.toString()); 24 | } else { 25 | log.info('Current user is undefined'); 26 | } 27 | 28 | return user; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/database/Copia de migrations/1511105183653-CreateUserTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | import { fixForPostgres } from '../../lib/env/utils'; 3 | 4 | export class CreateUserTable1511105183653 implements MigrationInterface { 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | const table = new Table({ 8 | name: 'user', 9 | columns: fixForPostgres([ 10 | { 11 | name: 'id', 12 | type: 'varchar', 13 | length: '255', 14 | isPrimary: true, 15 | isNullable: false, 16 | }, { 17 | name: 'first_name', 18 | type: 'varchar', 19 | length: '255', 20 | isPrimary: false, 21 | isNullable: false, 22 | }, { 23 | name: 'last_name', 24 | type: 'varchar', 25 | length: '255', 26 | isPrimary: false, 27 | isNullable: false, 28 | }, { 29 | name: 'email', 30 | type: 'varchar', 31 | length: '255', 32 | isPrimary: false, 33 | isNullable: false, 34 | }, 35 | ]), 36 | }); 37 | await queryRunner.createTable(table); 38 | } 39 | 40 | public async down(queryRunner: QueryRunner): Promise { 41 | await queryRunner.dropTable('user'); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/database/Copia de migrations/1512663524808-CreatePetTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | import { fixForPostgres } from '../../lib/env/utils'; 3 | export class CreatePetTable1512663524808 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | const table = new Table({ 7 | name: 'pet', 8 | columns: fixForPostgres([ 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 | // length: '11', 25 | isPrimary: false, 26 | isNullable: false, 27 | }, { 28 | name: 'user_id', 29 | type: 'varchar', 30 | length: '255', 31 | isPrimary: false, 32 | isNullable: true, 33 | }, 34 | ]), 35 | }); 36 | await queryRunner.createTable(table); 37 | } 38 | 39 | public async down(queryRunner: QueryRunner): Promise { 40 | await queryRunner.dropTable('pet'); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/database/Copia de 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/factories/PetFactory.ts: -------------------------------------------------------------------------------- 1 | import * as Faker from 'faker'; 2 | 3 | import { Pet } from '../../../src/api/models/Pet'; 4 | import { define } from '../../lib/seed'; 5 | 6 | define(Pet, (faker: typeof Faker) => { 7 | const gender = faker.random.number(1); 8 | const name = faker.name.firstName(gender); 9 | 10 | const pet = new Pet(); 11 | pet.name = name; 12 | pet.age = faker.random.number(); 13 | return pet; 14 | }); 15 | -------------------------------------------------------------------------------- /src/database/factories/UserFactory.ts: -------------------------------------------------------------------------------- 1 | import * as Faker from 'faker'; 2 | 3 | import { User } from '../../../src/api/models/User'; 4 | import { define } from '../../lib/seed'; 5 | 6 | define(User, (faker: typeof Faker, settings: { role: string }) => { 7 | const gender = faker.random.number(1); 8 | const firstName = faker.name.firstName(gender); 9 | const lastName = faker.name.lastName(gender); 10 | const email = faker.internet.email(firstName, lastName); 11 | 12 | const user = new User(); 13 | user.firstName = firstName; 14 | user.lastName = lastName; 15 | user.email = email; 16 | return user; 17 | }); 18 | -------------------------------------------------------------------------------- /src/database/seeds/CreateBruce.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'typeorm'; 2 | 3 | import { User } from '../../../src/api/models/User'; 4 | import { Factory, Seed } from '../../lib/seed/types'; 5 | 6 | export class CreateBruce implements Seed { 7 | 8 | public async seed(factory: Factory, connection: Connection): Promise { 9 | // const userFactory = factory(User as any); 10 | // const adminUserFactory = userFactory({ role: 'admin' }); 11 | 12 | // const bruce = await adminUserFactory.make(); 13 | // console.log(bruce); 14 | 15 | // const bruce2 = await adminUserFactory.seed(); 16 | // console.log(bruce2); 17 | 18 | // const bruce3 = await adminUserFactory 19 | // .map(async (e: User) => { 20 | // e.firstName = 'Bruce'; 21 | // return e; 22 | // }) 23 | // .seed(); 24 | // console.log(bruce3); 25 | 26 | // return bruce; 27 | 28 | // const connection = await factory.getConnection(); 29 | const em = connection.createEntityManager(); 30 | 31 | const user = new User(); 32 | user.firstName = 'Bruce'; 33 | user.lastName = 'Wayne'; 34 | user.email = 'bruce.wayne@wayne-enterprises.com'; 35 | return await em.save(user); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/database/seeds/CreatePets.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'typeorm'; 2 | 3 | import { Pet } from '../../../src/api/models/Pet'; 4 | import { User } from '../../../src/api/models/User'; 5 | import { Factory, Seed, times } from '../../lib/seed'; 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 { Connection } from 'typeorm/connection/Connection'; 2 | 3 | import { User } from '../../../src/api/models/User'; 4 | import { Factory, Seed } from '../../lib/seed/types'; 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/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): any { 6 | return (object: any, propertyName: string, index?: number): any => { 7 | const logger = new WinstonLogger(scope); 8 | Container.registerHandler({ object, propertyName, index, value: () => logger }); 9 | }; 10 | } 11 | 12 | export { LoggerInterface } from '../lib/logger'; 13 | -------------------------------------------------------------------------------- /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 { getOsEnv, getOsEnvArray, normalizePort, toBool, toNumber } from './lib/env'; 6 | 7 | /** 8 | * Load .env file or for tests the .env.test file. 9 | */ 10 | dotenv.config({ path: path.join(process.cwd(), `.env${((process.env.NODE_ENV === 'test') ? '.test' : '')}`) }); 11 | 12 | /** 13 | * Environment variables 14 | */ 15 | export const env = { 16 | node: process.env.NODE_ENV || 'development', 17 | isProduction: process.env.NODE_ENV === 'production', 18 | isTest: process.env.NODE_ENV === 'test', 19 | isDevelopment: process.env.NODE_ENV === 'development', 20 | app: { 21 | name: getOsEnv('APP_NAME'), 22 | version: (pkg as any).version, 23 | description: (pkg as any).description, 24 | host: getOsEnv('APP_HOST'), 25 | schema: getOsEnv('APP_SCHEMA'), 26 | routePrefix: getOsEnv('APP_ROUTE_PREFIX'), 27 | port: normalizePort(process.env.PORT || getOsEnv('APP_PORT')), 28 | banner: toBool(getOsEnv('APP_BANNER')), 29 | dirs: { 30 | migrations: ( 31 | getOsEnvArray('TYPEORM_MIGRATIONS') || 32 | [path.relative(path.join(process.cwd()), path.join(__dirname, 'database/migrations/*.ts'))] 33 | ) as string[], 34 | migrationsDir: getOsEnv('TYPEORM_MIGRATIONS_DIR') || path.relative(path.join(process.cwd()), path.join(__dirname, 'database/migrations')), 35 | entities: ( 36 | getOsEnvArray('TYPEORM_ENTITIES') || 37 | [path.relative(path.join(process.cwd()), path.join(__dirname, 'api/models/**/*{.js,.ts}'))] 38 | ) as string[], 39 | subscribers: ( 40 | getOsEnvArray('TYPEORM_SUBSCRIBERS') || 41 | [path.join(__dirname, 'api/subscribers/**/*Subscriber{.js,.ts}')] 42 | ) as string[], 43 | controllers: ( 44 | getOsEnvArray('CONTROLLERS') || 45 | [path.join(__dirname, 'api/controllers/**/*Controller{.js,.ts}')] 46 | ) as string[], 47 | middlewares: ( 48 | getOsEnvArray('MIDDLEWARES') || 49 | [path.join(__dirname, 'api/middlewares/**/*Middleware{.js,.ts}')] 50 | ) as string[], 51 | interceptors: ( 52 | getOsEnvArray('INTERCEPTORS') || 53 | [path.join(__dirname, 'api/interceptors/**/*Interceptor{.js,.ts}')] 54 | ) as string[], 55 | queries: ( 56 | getOsEnvArray('QUERIES') || 57 | [path.join(__dirname, 'api/queries/**/*Query{.js,.ts}')] 58 | ) as string[], 59 | mutations: ( 60 | getOsEnvArray('MUTATIONS') || 61 | [path.join(__dirname, 'api/mutations/**/*Mutation{.js,.ts}')] 62 | ) as string[], 63 | }, 64 | }, 65 | log: { 66 | level: getOsEnv('LOG_LEVEL'), 67 | json: toBool(getOsEnv('LOG_JSON')), 68 | output: getOsEnv('LOG_OUTPUT'), 69 | }, 70 | auth: { 71 | route: getOsEnv('AUTH_ROUTE'), 72 | }, 73 | db: { 74 | type: getOsEnv('TYPEORM_CONNECTION_TYPE'), 75 | host: getOsEnv('TYPEORM_HOST'), 76 | port: toNumber(getOsEnv('TYPEORM_PORT')), 77 | username: getOsEnv('TYPEORM_USERNAME'), 78 | password: getOsEnv('TYPEORM_PASSWORD'), 79 | database: getOsEnv('TYPEORM_DATABASE'), 80 | synchronize: toBool(getOsEnv('TYPEORM_SYNCHRONIZE')), 81 | logging: toBool(getOsEnv('TYPEORM_LOGGING')), 82 | }, 83 | graphql: { 84 | enabled: toBool(getOsEnv('GRAPHQL_ENABLED')), 85 | route: getOsEnv('GRAPHQL_ROUTE'), 86 | editor: toBool(getOsEnv('GRAPHQL_EDITOR')), 87 | }, 88 | swagger: { 89 | enabled: toBool(getOsEnv('SWAGGER_ENABLED')), 90 | route: getOsEnv('SWAGGER_ROUTE'), 91 | file: getOsEnv('SWAGGER_FILE'), 92 | username: getOsEnv('SWAGGER_USERNAME'), 93 | password: getOsEnv('SWAGGER_PASSWORD'), 94 | }, 95 | monitor: { 96 | enabled: toBool(getOsEnv('MONITOR_ENABLED')), 97 | route: getOsEnv('MONITOR_ROUTE'), 98 | username: getOsEnv('MONITOR_USERNAME'), 99 | password: getOsEnv('MONITOR_PASSWORD'), 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /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 | export function getOsEnv(key: string): string { 2 | return process.env[key] as string; 3 | } 4 | 5 | export function getOsEnvArray(key: string, delimiter: string = ','): string[] | boolean { 6 | return process.env[key] && process.env[key].split(delimiter) || false; 7 | } 8 | 9 | export function toNumber(value: string): number { 10 | return parseInt(value, 10); 11 | } 12 | 13 | export function toBool(value: string): boolean { 14 | return value === 'true'; 15 | } 16 | 17 | export function normalizePort(port: string): number | string | boolean { 18 | const parsedPort = parseInt(port, 10); 19 | if (isNaN(parsedPort)) { // named pipe 20 | return port; 21 | } 22 | if (parsedPort >= 0) { // port number 23 | return parsedPort; 24 | } 25 | return false; 26 | } 27 | 28 | export function fixForPostgres(cols: any[]): any[] { 29 | return cols.map(col => { 30 | if (getOsEnv('TYPEORM_CONNECTION_TYPE') === 'postgres') { 31 | if (col.name.match('id')) { 32 | col.type = 'uuid'; 33 | col.generationStrategy = 'uuid'; 34 | col.isGenerated = true; 35 | delete col.length; 36 | if (col.name.match('_id')) { 37 | col.isNullable = true; 38 | delete col.isGenerated; 39 | delete col.generationStrategy; 40 | } 41 | return col; 42 | } 43 | if (col.type.match('int')) { 44 | delete col.length; 45 | return col; 46 | } 47 | } 48 | return col; 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/graphql/AbstractGraphQLHooks.ts: -------------------------------------------------------------------------------- 1 | import { UserError } from './graphql-error-handling'; 2 | 3 | export abstract class AbstractGraphQLHooks { 4 | 5 | /** 6 | * This is our before hook. Here we are able 7 | * to alter the args object before the actual resolver(execute) 8 | * will be called. 9 | */ 10 | public before(context: TContext, args: TArgs, source?: S): Promise | TArgs { 11 | return args; 12 | } 13 | 14 | /** 15 | * This is our after hook. It will be called ater the actual resolver(execute). 16 | * There you are able to alter the result before it is send to the client. 17 | */ 18 | public after(result: TResult, context: TContext, args?: TArgs, source?: S): Promise | TResult { 19 | return result; 20 | } 21 | 22 | /** 23 | * This is our resolver, which should gather the needed data; 24 | */ 25 | public run(rootOrSource: S, args: TArgs, context: TContext): Promise | TResult { 26 | throw new UserError('Query not implemented!'); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/graphql/AbstractGraphQLMutation.ts: -------------------------------------------------------------------------------- 1 | import { AbstractGraphQLQuery } from './AbstractGraphQLQuery'; 2 | 3 | export abstract class AbstractGraphQLMutation extends AbstractGraphQLQuery { 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/graphql/AbstractGraphQLQuery.ts: -------------------------------------------------------------------------------- 1 | import { AbstractGraphQLHooks } from './AbstractGraphQLHooks'; 2 | 3 | export abstract class AbstractGraphQLQuery extends AbstractGraphQLHooks { 4 | 5 | /** 6 | * This will be called by graphQL and they need it as a property function. 7 | * We use this hook to add some more logic to it, 8 | * like permission checking, before- and after hooks to alter some data. 9 | */ 10 | public resolve = async (root: S, args: TArgs, context: TContext): Promise => { 11 | // We need to store the query arguments in the context so they can be accessed by subsequent resolvers 12 | (context as any).resolveArgs = args; 13 | args = await this.before(context, args); 14 | let result = await this.run(root, args, context); 15 | result = await this.after(result, context, args); 16 | return result as TResult; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/graphql/GraphQLContext.ts: -------------------------------------------------------------------------------- 1 | import * as DataLoader from 'dataloader'; 2 | import * as express from 'express'; 3 | import { Container } from 'typedi'; 4 | 5 | export interface GraphQLContext { 6 | container: typeof Container; 7 | request: express.Request; 8 | response: express.Response; 9 | dataLoaders: GraphQLContextDataLoader; 10 | resolveArgs?: TResolveArgs; 11 | data?: TData; 12 | } 13 | 14 | export interface GraphQLContextDataLoader { 15 | [key: string]: DataLoader; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/graphql/MetadataArgsStorage.ts: -------------------------------------------------------------------------------- 1 | import { MutationMetadataArgs } from './MutationMetadataArgs'; 2 | import { QueryMetadataArgs } from './QueryMetadataArgs'; 3 | 4 | /** 5 | * Storage all metadatas read from decorators. 6 | */ 7 | export class MetadataArgsStorage { 8 | 9 | // ------------------------------------------------------------------------- 10 | // Properties 11 | // ------------------------------------------------------------------------- 12 | 13 | /** 14 | * Registered controller metadata args. 15 | */ 16 | public queries: QueryMetadataArgs[] = []; 17 | 18 | /** 19 | * Registered middleware metadata args. 20 | */ 21 | public mutations: MutationMetadataArgs[] = []; 22 | 23 | // ------------------------------------------------------------------------- 24 | // Public Methods 25 | // ------------------------------------------------------------------------- 26 | /** 27 | * Filters registered queries by a given classes. 28 | */ 29 | public filterQueryMetadatasForClasses(classes: Array<() => void>): MutationMetadataArgs[] { 30 | return this.queries.filter(ctrl => { 31 | return classes.filter(cls => ctrl.target === cls).length > 0; 32 | }); 33 | } 34 | /** 35 | * Filters registered mutations by a given classes. 36 | */ 37 | public filterMutationMetadatasForClasses(classes: Array<() => void>): MutationMetadataArgs[] { 38 | return this.mutations.filter(ctrl => { 39 | return classes.filter(cls => ctrl.target === cls).length > 0; 40 | }); 41 | } 42 | 43 | /** 44 | * Removes all saved metadata. 45 | */ 46 | public reset(): void { 47 | this.queries = []; 48 | this.mutations = []; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/graphql/Mutation.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataArgsStorage } from './index'; 2 | 3 | export function Mutation(): any { 4 | return (object: () => void) => { 5 | getMetadataArgsStorage().mutations.push({ 6 | name: object.name, 7 | target: object, 8 | }); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/graphql/MutationMetadataArgs.ts: -------------------------------------------------------------------------------- 1 | export interface MutationMetadataArgs { 2 | name: string; 3 | /** 4 | * Indicates object which is used by this controller. 5 | */ 6 | target: () => void; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/graphql/Query.ts: -------------------------------------------------------------------------------- 1 | import { getMetadataArgsStorage } from './index'; 2 | 3 | export function Query(): any { 4 | return (object: () => void) => { 5 | getMetadataArgsStorage().queries.push({ 6 | name: object.name, 7 | target: object, 8 | }); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/graphql/QueryMetadataArgs.ts: -------------------------------------------------------------------------------- 1 | export interface QueryMetadataArgs { 2 | name: string; 3 | /** 4 | * Indicates object which is used by this controller. 5 | */ 6 | target: () => void; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/graphql/container.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Container options. 4 | */ 5 | export interface UseContainerOptions { 6 | 7 | /** 8 | * If set to true, then default container will be used in the case if given container haven't returned anything. 9 | */ 10 | fallback?: boolean; 11 | 12 | /** 13 | * If set to true, then default container will be used in the case if given container thrown an exception. 14 | */ 15 | fallbackOnErrors?: boolean; 16 | 17 | } 18 | 19 | /** 20 | * Container to be used by this library for inversion control. If container was not implicitly set then by default 21 | * container simply creates a new instance of the given class. 22 | */ 23 | const defaultContainer: { get(someClass: { new(...args: any[]): T } | (() => void)): T } = new (class { 24 | private instances: Array<{ type: any, object: any }> = []; 25 | public get(someClass: { new(...args: any[]): T }): T { 26 | let instance = this.instances.find(i => i.type === someClass); 27 | if (!instance) { 28 | instance = { type: someClass, object: new someClass() }; 29 | this.instances.push(instance); 30 | } 31 | 32 | return instance.object; 33 | } 34 | })(); 35 | 36 | let userContainer: { get(someClass: { new(...args: any[]): T } | (() => void)): T }; 37 | let userContainerOptions: UseContainerOptions; 38 | 39 | /** 40 | * Sets container to be used by this library. 41 | */ 42 | export function useContainer(iocContainer: { get(someClass: any): any }, options?: UseContainerOptions): void { 43 | userContainer = iocContainer; 44 | if (options) { 45 | userContainerOptions = options; 46 | } 47 | } 48 | 49 | /** 50 | * Gets the IOC container used by this library. 51 | */ 52 | export function getFromContainer(someClass: { new(...args: any[]): T } | (() => void)): T { 53 | if (userContainer) { 54 | try { 55 | const instance = userContainer.get(someClass); 56 | if (instance) { 57 | return instance; 58 | } 59 | 60 | if (!userContainerOptions || !userContainerOptions.fallback) { 61 | return instance; 62 | } 63 | 64 | } catch (error) { 65 | if (!userContainerOptions || !userContainerOptions.fallbackOnErrors) { 66 | throw error; 67 | } 68 | } 69 | } 70 | return defaultContainer.get(someClass); 71 | } 72 | -------------------------------------------------------------------------------- /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/importClassesFromDirectories.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | /** 4 | * Loads all exported classes from the given directory. 5 | */ 6 | export function importClassesFromDirectories(directories: string[], formats: string[] = ['.js', '.ts']): Array<() => void> { 7 | 8 | const loadFileClasses = (exported: any, allLoaded: Array<() => void>) => { 9 | if (exported instanceof Function) { 10 | allLoaded.push(exported); 11 | } else if (exported instanceof Array) { 12 | exported.forEach((i: any) => loadFileClasses(i, allLoaded)); 13 | } else if (exported instanceof Object || typeof exported === 'object') { 14 | Object.keys(exported).forEach(key => loadFileClasses(exported[key], allLoaded)); 15 | } 16 | 17 | return allLoaded; 18 | }; 19 | 20 | const allFiles = directories.reduce((allDirs, dir) => { 21 | return allDirs.concat(require('glob').sync(path.normalize(dir))); 22 | }, [] as string[]); 23 | 24 | const dirs = allFiles 25 | .filter(file => { 26 | const dtsExtension = file.substring(file.length - 5, file.length); 27 | return formats.indexOf(path.extname(file)) !== -1 && dtsExtension !== '.d.ts'; 28 | }) 29 | .map(file => { 30 | return require(file); 31 | }); 32 | 33 | return loadFileClasses(dirs, []); 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import * as DataLoader from 'dataloader'; 2 | import * as express from 'express'; 3 | import * as GraphQLHTTP from 'express-graphql'; 4 | import { GraphQLObjectType, GraphQLSchema } from 'graphql'; 5 | import { Container as container, ObjectType } from 'typedi'; 6 | import { getCustomRepository, getRepository, Repository } from 'typeorm'; 7 | 8 | import { getFromContainer } from './container'; 9 | import { getErrorCode, getErrorMessage, handlingErrors } from './graphql-error-handling'; 10 | import { GraphQLContext, GraphQLContextDataLoader } from './GraphQLContext'; 11 | import { importClassesFromDirectories } from './importClassesFromDirectories'; 12 | import { MetadataArgsStorage } from './MetadataArgsStorage'; 13 | 14 | // ------------------------------------------------------------------------- 15 | // Main exports 16 | // ------------------------------------------------------------------------- 17 | 18 | export * from './Query'; 19 | export * from './Mutation'; 20 | 21 | export * from './AbstractGraphQLHooks'; 22 | export * from './AbstractGraphQLQuery'; 23 | export * from './GraphQLContext'; 24 | export * from './graphql-error-handling'; 25 | export * from './container'; 26 | 27 | // ------------------------------------------------------------------------- 28 | // Main Functions 29 | // ------------------------------------------------------------------------- 30 | 31 | export interface CreateDataLoaderOptions { 32 | method?: string; 33 | key?: string; 34 | multiple?: boolean; 35 | } 36 | 37 | /** 38 | * Creates a new dataloader with the typorm repository 39 | */ 40 | export function createDataLoader(obj: ObjectType, options: CreateDataLoaderOptions = {}): DataLoader { 41 | let repository; 42 | try { 43 | repository = getCustomRepository>(obj); 44 | } catch (errorRepo) { 45 | try { 46 | repository = getRepository(obj); 47 | } catch (errorModel) { 48 | throw new Error('Could not create a dataloader, because obj is nether model or repository!'); 49 | } 50 | } 51 | 52 | return new DataLoader(async (ids: number[]) => { 53 | let items = []; 54 | if (options.method) { 55 | items = await repository[options.method](ids); 56 | } else { 57 | items = await repository.findByIds(ids); 58 | } 59 | 60 | const handleBatch = (arr: any[]) => options.multiple === true ? arr : arr[0]; 61 | return ids.map(id => handleBatch(items.filter(item => item[options.key || 'id'] === id))); 62 | }); 63 | } 64 | 65 | /** 66 | * Defines the options to create a GraphQLServer 67 | */ 68 | export interface GraphQLServerOptions { 69 | queries: string[]; 70 | mutations: string[]; 71 | route?: string; 72 | dataLoaders?: GraphQLContextDataLoader; 73 | editorEnabled?: boolean; 74 | contextData?: TData; 75 | } 76 | 77 | /** 78 | * Create GraphQL Server and bind it to the gieven express app 79 | */ 80 | export function createGraphQLServer(expressApp: express.Application, options: GraphQLServerOptions): void { 81 | // collect queries & mutaions for our graphql schema 82 | const schema = createSchema({ 83 | queries: options.queries, 84 | mutations: options.mutations, 85 | }); 86 | 87 | // Handles internal errors and prints the stack to the console 88 | handlingErrors(schema); 89 | 90 | // Add graphql layer to the express app 91 | expressApp.use(options.route || '/graphql', (request: express.Request, response: express.Response) => { 92 | 93 | // Build GraphQLContext 94 | const context: GraphQLContext = { 95 | container, 96 | request, 97 | response, 98 | dataLoaders: options.dataLoaders || {}, 99 | resolveArgs: {}, 100 | data: options.contextData, 101 | }; 102 | 103 | // Setup GraphQL Server 104 | GraphQLHTTP({ 105 | schema, 106 | context, 107 | graphiql: options.editorEnabled || true, 108 | formatError: error => ({ 109 | code: getErrorCode(error.message), 110 | message: getErrorMessage(error.message), 111 | path: error.path, 112 | }), 113 | })(request, response); 114 | }); 115 | } 116 | 117 | /** 118 | * Gets metadata args storage. 119 | * Metadata args storage follows the best practices and stores metadata in a global variable. 120 | */ 121 | export function getMetadataArgsStorage(): MetadataArgsStorage { 122 | if (!(global as any).graphqlMetadataArgsStorage) { 123 | (global as any).graphqlMetadataArgsStorage = new MetadataArgsStorage(); 124 | } 125 | 126 | return (global as any).graphqlMetadataArgsStorage; 127 | } 128 | 129 | /** 130 | * Create query name out of the class name 131 | */ 132 | export function createQueryName(name: string): string { 133 | return lowercaseFirstLetter(removeSuffix(name, 'Query')); 134 | } 135 | 136 | /** 137 | * Create mutation name out of the class name 138 | */ 139 | export function createMutationName(name: string): string { 140 | return lowercaseFirstLetter(removeSuffix(name, 'Mutation')); 141 | } 142 | 143 | /** 144 | * Removes the suffix 145 | */ 146 | export function removeSuffix(value: string, suffix: string): string { 147 | return value.slice(0, value.length - suffix.length); 148 | } 149 | 150 | /** 151 | * LowerCase first letter 152 | */ 153 | export function lowercaseFirstLetter(s: string): string { 154 | return s.charAt(0).toLowerCase() + s.slice(1); 155 | } 156 | 157 | /** 158 | * GraphQL schema options for building it 159 | */ 160 | export interface GraphQLSchemaOptions { 161 | queries: string[]; 162 | mutations: string[]; 163 | } 164 | 165 | /** 166 | * Create schema out of the @Query and @Mutation 167 | */ 168 | export function createSchema(options: GraphQLSchemaOptions): GraphQLSchema { 169 | 170 | // import all queries 171 | let queryClasses: Array<() => void> = []; 172 | if (options && options.queries && options.queries.length) { 173 | queryClasses = (options.queries as any[]).filter(query => query instanceof Function); 174 | const queryDirs = (options.queries as any[]).filter(query => typeof query === 'string'); 175 | queryClasses.push(...importClassesFromDirectories(queryDirs)); 176 | } 177 | 178 | const queries = {}; 179 | getMetadataArgsStorage().queries.forEach(queryMetdadata => { 180 | queries[createQueryName(queryMetdadata.name)] = getFromContainer(queryMetdadata.target); 181 | }); 182 | 183 | const RootQuery = new GraphQLObjectType({ 184 | name: 'Query', 185 | fields: queries, 186 | }); 187 | 188 | // import all mutations 189 | let mutationClasses: Array<() => void> = []; 190 | if (options && options.mutations && options.mutations.length) { 191 | mutationClasses = (options.mutations as any[]).filter(mutation => mutation instanceof Function); 192 | const mutationDirs = (options.mutations as any[]).filter(mutation => typeof mutation === 'string'); 193 | mutationClasses.push(...importClassesFromDirectories(mutationDirs)); 194 | } 195 | 196 | const mutations = {}; 197 | getMetadataArgsStorage().mutations.forEach(mutationMetdadata => { 198 | mutations[createMutationName(mutationMetdadata.name)] = getFromContainer(mutationMetdadata.target); 199 | }); 200 | 201 | const RootMutation: GraphQLObjectType = new GraphQLObjectType({ 202 | name: 'Mutation', 203 | fields: mutations, 204 | }); 205 | 206 | const schemaOptions: any = {}; 207 | 208 | if (queryClasses && queryClasses.length) { 209 | schemaOptions.query = RootQuery; 210 | } 211 | 212 | if (mutationClasses && mutationClasses.length) { 213 | schemaOptions.mutation = RootMutation; 214 | } 215 | 216 | return new GraphQLSchema(schemaOptions); 217 | 218 | } 219 | -------------------------------------------------------------------------------- /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/lib/seed/EntityFactory.ts: -------------------------------------------------------------------------------- 1 | import * as Faker from 'faker'; 2 | import { Connection, ObjectType } from 'typeorm'; 3 | 4 | import { FactoryFunction } from './types'; 5 | import { isPromiseLike } from './utils'; 6 | 7 | export class EntityFactory { 8 | 9 | private mapFunction: (entity: Entity) => Promise; 10 | 11 | constructor( 12 | public name: string, 13 | public entity: ObjectType, 14 | private factory: FactoryFunction, 15 | private settings?: Settings 16 | ) { } 17 | 18 | // ------------------------------------------------------------------------- 19 | // Public API 20 | // ------------------------------------------------------------------------- 21 | 22 | /** 23 | * This function is used to alter the generated values of entity, before it 24 | * is persist into the database 25 | */ 26 | public map(mapFunction: (entity: Entity) => Promise): EntityFactory { 27 | this.mapFunction = mapFunction; 28 | return this; 29 | } 30 | 31 | /** 32 | * Make a new entity, but does not persist it 33 | */ 34 | public async make(): Promise { 35 | if (this.factory) { 36 | let entity = await this.resolveEntity(this.factory(Faker, this.settings)); 37 | if (this.mapFunction) { 38 | entity = await this.mapFunction(entity); 39 | } 40 | return entity; 41 | } 42 | throw new Error('Could not found entity'); 43 | } 44 | 45 | /** 46 | * Seed makes a new entity and does persist it 47 | */ 48 | public async seed(): Promise { 49 | const connection: Connection = (global as any).seeder.connection; 50 | if (connection) { 51 | const em = connection.createEntityManager(); 52 | try { 53 | const entity = await this.make(); 54 | return await em.save(entity); 55 | } catch (error) { 56 | throw new Error('Could not save entity'); 57 | } 58 | } else { 59 | throw new Error('No db connection is given'); 60 | } 61 | } 62 | 63 | public async makeMany(amount: number): Promise { 64 | const list = []; 65 | for (let index = 0; index < amount; index++) { 66 | list[index] = await this.make(); 67 | } 68 | return list; 69 | } 70 | 71 | public async seedMany(amount: number): Promise { 72 | const list = []; 73 | for (let index = 0; index < amount; index++) { 74 | list[index] = await this.seed(); 75 | } 76 | return list; 77 | } 78 | 79 | // ------------------------------------------------------------------------- 80 | // Prrivat Helpers 81 | // ------------------------------------------------------------------------- 82 | 83 | private async resolveEntity(entity: Entity): Promise { 84 | for (const attribute in entity) { 85 | if (entity.hasOwnProperty(attribute)) { 86 | if (isPromiseLike(entity[attribute])) { 87 | entity[attribute] = await entity[attribute]; 88 | } 89 | 90 | if (typeof entity[attribute] === 'object') { 91 | const subEntityFactory = entity[attribute]; 92 | try { 93 | entity[attribute] = await (subEntityFactory as any).make(); 94 | } catch (e) { 95 | throw new Error(`Could not make ${(subEntityFactory as any).name}`); 96 | } 97 | } 98 | } 99 | } 100 | return entity; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/lib/seed/cli.ts: -------------------------------------------------------------------------------- 1 | import * as Chalk from 'chalk'; 2 | import * as commander from 'commander'; 3 | import * as path from 'path'; 4 | 5 | import { loadEntityFactories } from './'; 6 | import { getConnection } from './connection'; 7 | import { loadSeeds } from './importer'; 8 | import { runSeed, setConnection } from './index'; 9 | 10 | // Cli helper 11 | commander 12 | .version('1.0.0') 13 | .description('Run database seeds of your project') 14 | .option('-L, --logging', 'enable sql query logging') 15 | .option('--factories ', 'add filepath for your factories') 16 | .option('--seeds ', 'add filepath for your seeds') 17 | .option('--run ', 'run specific seeds (file names without extension)', (val) => val.split(',')) 18 | .option('--config ', 'path to your ormconfig.json file (must be a json)') 19 | .parse(process.argv); 20 | 21 | // Get cli parameter for a different factory path 22 | const factoryPath = (commander.factories) 23 | ? commander.factories 24 | : 'src/database/'; 25 | 26 | // Get cli parameter for a different seeds path 27 | const seedsPath = (commander.seeds) 28 | ? commander.seeds 29 | : 'src/database/seeds/'; 30 | 31 | // Get a list of seeds 32 | const listOfSeeds = (commander.run) 33 | ? commander.run.map(l => l.trim()).filter(l => l.length > 0) 34 | : []; 35 | 36 | // Search for seeds and factories 37 | const run = async () => { 38 | const log = console.log; 39 | const chalk = Chalk.default; 40 | 41 | let factoryFiles; 42 | let seedFiles; 43 | try { 44 | factoryFiles = await loadEntityFactories(factoryPath); 45 | seedFiles = await loadSeeds(seedsPath); 46 | } catch (error) { 47 | return handleError(error); 48 | } 49 | 50 | // Filter seeds 51 | if (listOfSeeds.length > 0) { 52 | seedFiles = seedFiles.filter(sf => listOfSeeds.indexOf(path.basename(sf).replace('.ts', '')) >= 0); 53 | } 54 | 55 | // Status logging to print out the amount of factories and seeds. 56 | log(chalk.bold('seeds')); 57 | log('🔎 ', chalk.gray.underline(`found:`), 58 | chalk.blue.bold(`${factoryFiles.length} factories`, chalk.gray('&'), chalk.blue.bold(`${seedFiles.length} seeds`))); 59 | 60 | // Get database connection and pass it to the seeder 61 | let connection; 62 | try { 63 | connection = await getConnection(); 64 | setConnection(connection); 65 | } catch (error) { 66 | return handleError(error); 67 | } 68 | 69 | // Show seeds in the console 70 | for (const seedFile of seedFiles) { 71 | try { 72 | let className = seedFile.split('/')[seedFile.split('/').length - 1]; 73 | className = className.replace('.ts', '').replace('.js', ''); 74 | className = className.split('-')[className.split('-').length - 1]; 75 | log('\n' + chalk.gray.underline(`executing seed: `), chalk.green.bold(`${className}`)); 76 | const seedFileObject: any = require(seedFile); 77 | await runSeed(seedFileObject[className]); 78 | } catch (error) { 79 | console.error('Could not run seed ', error); 80 | process.exit(1); 81 | } 82 | } 83 | 84 | log('\n👍 ', chalk.gray.underline(`finished seeding`)); 85 | process.exit(0); 86 | }; 87 | 88 | const handleError = (error) => { 89 | console.error(error); 90 | process.exit(1); 91 | }; 92 | 93 | run(); 94 | -------------------------------------------------------------------------------- /src/lib/seed/connection.ts: -------------------------------------------------------------------------------- 1 | import { Connection, createConnection } from 'typeorm'; 2 | 3 | const args = process.argv; 4 | const runDir = process.cwd(); 5 | 6 | // Get cli parameter for logging 7 | const logging = args.indexOf('--logging') >= 0 || args.indexOf('-L') >= 0 || false; 8 | 9 | // Get cli parameter for ormconfig.json or another json file 10 | const configParam = '--config'; 11 | const hasConfigPath = args.indexOf(configParam) >= 0 || false; 12 | const indexOfConfigPath = args.indexOf(configParam) + 1; 13 | 14 | /** 15 | * Returns a TypeORM database connection for our entity-manager 16 | */ 17 | export const getConnection = async (): Promise => { 18 | const ormconfig = (hasConfigPath) 19 | ? require(`${args[indexOfConfigPath]}`) 20 | : require(`${runDir}/ormconfig.json`); 21 | ormconfig.logging = logging; 22 | 23 | return createConnection(ormconfig); 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/seed/importer.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob'; 2 | import * as path from 'path'; 3 | 4 | // ------------------------------------------------------------------------- 5 | // Util functions 6 | // ------------------------------------------------------------------------- 7 | 8 | const importFactories = (files: string[]) => files.forEach(require); 9 | 10 | const loadFiles = 11 | (filePattern: string) => 12 | (pathToFolder: string) => 13 | (successFn: (files: string[]) => void) => 14 | (failedFn: (error: any) => void) => { 15 | glob(path.join(process.cwd(), pathToFolder, filePattern), (error: any, files: string[]) => error 16 | ? failedFn(error) 17 | : successFn(files)); 18 | }; 19 | 20 | const loadFactoryFiles = loadFiles('**/*Factory{.js,.ts}'); 21 | 22 | // ------------------------------------------------------------------------- 23 | // Facade functions 24 | // ------------------------------------------------------------------------- 25 | 26 | export const loadEntityFactories = (pathToFolder: string): Promise => { 27 | return new Promise((resolve, reject) => { 28 | loadFactoryFiles(pathToFolder)(files => { 29 | importFactories(files); 30 | resolve(files); 31 | })(reject); 32 | }); 33 | }; 34 | 35 | export const loadSeeds = (pathToFolder: string): Promise => { 36 | return new Promise((resolve, reject) => { 37 | loadFiles('**/*{.js,.ts}')(pathToFolder)(resolve)(reject); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/seed/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Connection, ObjectType } from 'typeorm'; 3 | 4 | import { EntityFactory } from './EntityFactory'; 5 | import { EntityFactoryDefinition, Factory, FactoryFunction, SeedConstructor } from './types'; 6 | import { getNameOfClass } from './utils'; 7 | 8 | // ------------------------------------------------------------------------- 9 | // Handy Exports 10 | // ------------------------------------------------------------------------- 11 | 12 | export * from './importer'; 13 | export { Factory, Seed } from './types'; 14 | export { times } from './utils'; 15 | 16 | // ------------------------------------------------------------------------- 17 | // Types & Variables 18 | // ------------------------------------------------------------------------- 19 | 20 | (global as any).seeder = { 21 | connection: undefined, 22 | entityFactories: new Map>(), 23 | }; 24 | 25 | // ------------------------------------------------------------------------- 26 | // Facade functions 27 | // ------------------------------------------------------------------------- 28 | 29 | /** 30 | * Adds the typorm connection to the seed options 31 | */ 32 | export const setConnection = (connection: Connection) => (global as any).seeder.connection = connection; 33 | 34 | /** 35 | * Returns the typorm connection from our seed options 36 | */ 37 | export const getConnection = () => (global as any).seeder.connection; 38 | 39 | /** 40 | * Defines a new entity factory 41 | */ 42 | export const define = (entity: ObjectType, factoryFn: FactoryFunction) => { 43 | (global as any).seeder.entityFactories.set(getNameOfClass(entity), { entity, factory: factoryFn }); 44 | }; 45 | 46 | /** 47 | * Gets a defined entity factory and pass the settigns along to the entity factory function 48 | */ 49 | export const factory: Factory = (entity: ObjectType) => (settings?: Settings) => { 50 | const name = getNameOfClass(entity); 51 | const entityFactoryObject = (global as any).seeder.entityFactories.get(name); 52 | return new EntityFactory( 53 | name, 54 | entity, 55 | entityFactoryObject.factory, 56 | settings 57 | ); 58 | }; 59 | 60 | /** 61 | * Runs a seed class 62 | */ 63 | export const runSeed = async (seederConstructor: SeedConstructor): Promise => { 64 | const seeder = new seederConstructor(); 65 | return seeder.seed(factory, getConnection()); 66 | }; 67 | -------------------------------------------------------------------------------- /src/lib/seed/types.ts: -------------------------------------------------------------------------------- 1 | import * as Faker from 'faker'; 2 | import { Connection, ObjectType } from 'typeorm'; 3 | 4 | import { EntityFactory } from './EntityFactory'; 5 | 6 | /** 7 | * FactoryFunction is the fucntion, which generate a new filled entity 8 | */ 9 | export type FactoryFunction = (faker: typeof Faker, settings?: Settings) => Entity; 10 | 11 | /** 12 | * Factory gets the EntityFactory to the given Entity and pass the settings along 13 | */ 14 | export type Factory = (entity: ObjectType) => (settings?: Settings) => EntityFactory; 15 | 16 | /** 17 | * Seed are the class to create some data. Those seed are run by the cli. 18 | */ 19 | export interface Seed { 20 | seed(factory: Factory, connection: Connection): Promise; 21 | } 22 | 23 | /** 24 | * Constructor of the seed class 25 | */ 26 | export interface SeedConstructor { 27 | new(): Seed; 28 | } 29 | 30 | /** 31 | * Value of our EntityFactory state 32 | */ 33 | export interface EntityFactoryDefinition { 34 | entity: ObjectType; 35 | factory: FactoryFunction; 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/seed/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the name of a class 3 | */ 4 | export const getNameOfClass = (c: any): string => new c().constructor.name; 5 | 6 | /** 7 | * Checks if the given argument is a promise 8 | */ 9 | export const isPromiseLike = (o: any): boolean => !!o && (typeof o === 'object' || typeof o === 'function') && typeof o.then === 'function'; 10 | 11 | /** 12 | * Times repeats a function n times 13 | */ 14 | export const times = async (n: number, iteratee: (index: number) => Promise): Promise => { 15 | const rs = [] as TResult[]; 16 | for (let i = 0; i < n; i++) { 17 | const r = await iteratee(i); 18 | rs.push(r); 19 | } 20 | return rs; 21 | }; 22 | -------------------------------------------------------------------------------- /src/loaders/eventDispatchLoader.ts: -------------------------------------------------------------------------------- 1 | import * as 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 { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 2 | 3 | import { Pet } from '../api/models/Pet'; 4 | import { PetRepository } from '../api/repositories/PetRepository'; 5 | import { UserRepository } from '../api/repositories/UserRepository'; 6 | import { env } from '../env'; 7 | import { createDataLoader, createGraphQLServer } from '../lib/graphql'; 8 | 9 | export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 10 | if (settings && env.graphql.enabled) { 11 | const expressApp = settings.getData('express_app'); 12 | 13 | createGraphQLServer(expressApp, { 14 | route: env.graphql.route, 15 | editorEnabled: env.graphql.editor, 16 | queries: env.app.dirs.queries, 17 | mutations: env.app.dirs.mutations, 18 | dataLoaders: { 19 | user: createDataLoader(UserRepository), 20 | pet: createDataLoader(Pet), 21 | petsByUserIds: createDataLoader(PetRepository, { 22 | method: 'findByUserIds', 23 | key: 'userId', 24 | multiple: true, 25 | }), 26 | }, 27 | }); 28 | 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /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 { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 2 | import { useContainer as routingUseContainer } from 'routing-controllers'; 3 | import { Container } from 'typedi'; 4 | import { useContainer as ormUseContainer } from 'typeorm'; 5 | import { useContainer as classValidatorUseContainer } from 'class-validator'; 6 | import { useContainer as graphqlUseContainer } from '../lib/graphql'; 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 | graphqlUseContainer(Container); 16 | classValidatorUseContainer(Container); 17 | }; 18 | -------------------------------------------------------------------------------- /src/loaders/monitorLoader.ts: -------------------------------------------------------------------------------- 1 | import * as basicAuth from 'express-basic-auth'; 2 | import * as 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 * as 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 filles 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 * as basicAuth from 'express-basic-auth'; 2 | import { MicroframeworkLoader, MicroframeworkSettings } from 'microframework-w3tec'; 3 | import * as path from 'path'; 4 | import * as swaggerUi from 'swagger-ui-express'; 5 | 6 | import { env } from '../env'; 7 | 8 | export const swaggerLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 9 | if (settings && env.swagger.enabled) { 10 | const expressApp = settings.getData('express_app'); 11 | const swaggerFile = require(path.join(__dirname, '..', env.swagger.file)); 12 | 13 | // Add npm infos to the swagger doc 14 | swaggerFile.info = { 15 | title: env.app.name, 16 | description: env.app.description, 17 | version: env.app.version, 18 | }; 19 | swaggerFile.host = `${env.app.host}:${env.app.port}`; 20 | swaggerFile.basePath = env.app.routePrefix; 21 | swaggerFile.schemes = [env.app.schema]; 22 | 23 | expressApp.use( 24 | env.swagger.route, 25 | env.swagger.username ? basicAuth({ 26 | users: { 27 | [`${env.swagger.username}`]: env.swagger.password, 28 | }, 29 | challenge: true, 30 | }) : (req, res, next) => next(), 31 | swaggerUi.serve, 32 | swaggerUi.setup(swaggerFile) 33 | ); 34 | 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /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 * as winston from 'winston'; 3 | 4 | import { env } from '../env'; 5 | 6 | export const winstonLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { 7 | winston.configure({ 8 | transports: [ 9 | new winston.transports.Console({ 10 | level: env.log.level, 11 | handleExceptions: true, 12 | json: env.log.json, 13 | timestamp: env.node !== 'development', 14 | colorize: env.node === 'development', 15 | }), 16 | ], 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeselaGen/typeorm-backend/2456e909f3a4b44d9be65e0a05d0eec1a23705b9/src/public/favicon.ico -------------------------------------------------------------------------------- /src/types/json.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | let json: any; 3 | export default json; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/api/info.test.ts: -------------------------------------------------------------------------------- 1 | import * as 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 * as request from 'supertest'; 3 | 4 | import { User } from '../../../src/api/models/User'; 5 | import { CreateBruce } from '../../../src/database/seeds/CreateBruce'; 6 | import { runSeed } from '../../../src/lib/seed'; 7 | import { closeDatabase } from '../../utils/database'; 8 | import { fakeAuthenticationForUser } from '../utils/auth'; 9 | import { BootstrapSettings } from '../utils/bootstrap'; 10 | import { prepareServer } from '../utils/server'; 11 | 12 | describe('/api/users', () => { 13 | 14 | let bruce: User; 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 | fakeAuthenticationForUser(bruce, true); 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', `Bearer 1234`) 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', `Bearer 1234`) 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/auth.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | 3 | import { User } from '../../../src/api/models/User'; 4 | import { env } from '../../../src/env'; 5 | 6 | export const fakeAuthenticationForUser = (user: User, persist = false): nock.Scope => { 7 | const scope = nock(env.auth.route) 8 | .post('') 9 | .reply(200, { 10 | user_id: `auth0|${user.email}`, 11 | }); 12 | if (persist) { 13 | scope.persist(); 14 | } 15 | return scope; 16 | }; 17 | -------------------------------------------------------------------------------- /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 '../../../src/lib/seed'; 2 | import { migrateDatabase } from '../../utils/database'; 3 | import { bootstrapApp } from './bootstrap'; 4 | 5 | export const prepareServer = async (options?: { migrate: boolean }) => { 6 | const settings = await bootstrapApp(); 7 | if (options && options.migrate) { 8 | await migrateDatabase(settings.connection); 9 | } 10 | setConnection(settings.connection); 11 | return settings; 12 | }; 13 | -------------------------------------------------------------------------------- /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 | 8 | describe('PetService', () => { 9 | 10 | // ------------------------------------------------------------------------- 11 | // Setup up 12 | // ------------------------------------------------------------------------- 13 | 14 | let connection: Connection; 15 | beforeAll(async () => connection = await createDatabaseConnection()); 16 | beforeEach(() => migrateDatabase(connection)); 17 | 18 | // ------------------------------------------------------------------------- 19 | // Tear down 20 | // ------------------------------------------------------------------------- 21 | 22 | afterAll(() => closeDatabase(connection)); 23 | 24 | // ------------------------------------------------------------------------- 25 | // Test cases 26 | // ------------------------------------------------------------------------- 27 | 28 | test('should create a new pet in the database', async (done) => { 29 | const pet = new Pet(); 30 | pet.name = 'test'; 31 | pet.age = 1; 32 | const service = Container.get(PetService); 33 | const resultCreate = await service.create(pet); 34 | expect(resultCreate.name).toBe(pet.name); 35 | expect(resultCreate.age).toBe(pet.age); 36 | 37 | const resultFind = await service.findOne(resultCreate.id); 38 | if (resultFind) { 39 | expect(resultFind.name).toBe(pet.name); 40 | expect(resultFind.age).toBe(pet.age); 41 | } else { 42 | fail('Could not find pet'); 43 | } 44 | done(); 45 | }); 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /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 * as MockExpressRequest from 'mock-express-request'; 3 | import * as nock from 'nock'; 4 | import * as request from 'request'; 5 | 6 | import { AuthService } from '../../../src/auth/AuthService'; 7 | import { env } from '../../../src/env'; 8 | import { LogMock } from '../lib/LogMock'; 9 | 10 | describe('AuthService', () => { 11 | 12 | let authService: AuthService; 13 | let log: LogMock; 14 | beforeEach(() => { 15 | log = new LogMock(); 16 | authService = new AuthService(log); 17 | }); 18 | 19 | describe('parseTokenFromRequest', () => { 20 | test('Should return the token without Bearer', () => { 21 | const req: Request = new MockExpressRequest({ 22 | headers: { 23 | Authorization: 'Bearer 1234', 24 | }, 25 | }); 26 | const token = authService.parseTokenFromRequest(req); 27 | expect(token).toBe('1234'); 28 | }); 29 | 30 | test('Should return undefined if there is no Bearer', () => { 31 | const req: Request = new MockExpressRequest({ 32 | headers: { 33 | Authorization: 'Basic 1234', 34 | }, 35 | }); 36 | const token = authService.parseTokenFromRequest(req); 37 | expect(token).toBeUndefined(); 38 | expect(log.infoMock).toBeCalledWith('No Token provided by the client', []); 39 | }); 40 | 41 | test('Should return undefined if there is no "Authorization" header', () => { 42 | const req: Request = new MockExpressRequest(); 43 | const token = authService.parseTokenFromRequest(req); 44 | expect(token).toBeUndefined(); 45 | expect(log.infoMock).toBeCalledWith('No Token provided by the client', []); 46 | }); 47 | }); 48 | 49 | describe('getTokenInfo', () => { 50 | test('Should get the tokeninfo', async (done) => { 51 | nock(env.auth.route) 52 | .post('') 53 | .reply(200, { 54 | user_id: 'auth0|test@test.com', 55 | }); 56 | 57 | const tokeninfo = await authService.getTokenInfo('1234'); 58 | expect(tokeninfo.user_id).toBe('auth0|test@test.com'); 59 | done(); 60 | }); 61 | 62 | test('Should fail due to invalid token', async (done) => { 63 | nock(env.auth.route) 64 | .post('') 65 | .reply(401, 'Invalid token'); 66 | 67 | try { 68 | await authService.getTokenInfo('1234'); 69 | } catch (error) { 70 | expect(error).toBe('Invalid token'); 71 | } 72 | done(); 73 | }); 74 | }); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /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 * as 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 | const errors = await validate(user); 40 | expect(errors.length).toEqual(0); 41 | done(); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /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 | export const createDatabaseConnection = async (): Promise => { 7 | useContainer(Container); 8 | const connection = await createConnection({ 9 | type: env.db.type as any, // See createConnection options for valid types 10 | database: env.db.database, 11 | logging: env.db.logging, 12 | entities: env.app.dirs.entities, 13 | migrations: env.app.dirs.migrations, 14 | }); 15 | return connection; 16 | }; 17 | 18 | export const synchronizeDatabase = async (connection: Connection) => { 19 | await connection.dropDatabase(); 20 | return connection.synchronize(true); 21 | }; 22 | 23 | export const migrateDatabase = async (connection: Connection) => { 24 | await connection.dropDatabase(); 25 | return connection.runMigrations(); 26 | }; 27 | 28 | export const closeDatabase = (connection: Connection) => { 29 | return connection.close(); 30 | }; 31 | -------------------------------------------------------------------------------- /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 | } 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "max-line-length": [ 5 | true, 6 | 160 7 | ], 8 | "no-unnecessary-initializer": false, 9 | "no-var-requires": true, 10 | "no-null-keyword": true, 11 | "no-consecutive-blank-lines": true, 12 | "quotemark": [ 13 | true, 14 | "single", 15 | "avoid-escape" 16 | ], 17 | "interface-name": false, 18 | "no-empty-interface": false, 19 | "no-namespace": false, 20 | "ordered-imports": false, 21 | "object-literal-sort-keys": false, 22 | "arrow-parens": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "public-static-field", 28 | "public-static-method", 29 | "protected-static-field", 30 | "protected-static-method", 31 | "private-static-field", 32 | "private-static-method", 33 | "public-instance-field", 34 | "protected-instance-field", 35 | "private-instance-field", 36 | "public-constructor", 37 | "protected-constructor", 38 | "private-constructor", 39 | "public-instance-method", 40 | "protected-instance-method", 41 | "private-instance-method" 42 | ] 43 | } 44 | ], 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-inferrable-types": [ 54 | true, 55 | "ignore-params" 56 | ], 57 | "no-switch-case-fall-through": true, 58 | "typedef": [ 59 | true, 60 | "call-signature", 61 | "parameter" 62 | ], 63 | "trailing-comma": [ 64 | true, 65 | { 66 | "multiline": { 67 | "objects": "always", 68 | "arrays": "always", 69 | "functions": "never", 70 | "typeLiterals": "ignore" 71 | }, 72 | "singleline": "never" 73 | } 74 | ], 75 | "align": [ 76 | true, 77 | "parameters" 78 | ], 79 | "class-name": true, 80 | "curly": true, 81 | "eofline": true, 82 | "jsdoc-format": true, 83 | "member-access": true, 84 | "no-arg": true, 85 | "no-construct": true, 86 | "no-duplicate-variable": true, 87 | "no-empty": true, 88 | "no-eval": true, 89 | "no-internal-module": true, 90 | "no-string-literal": true, 91 | "no-trailing-whitespace": true, 92 | "no-unused-expression": true, 93 | "no-var-keyword": true, 94 | "one-line": [ 95 | true, 96 | "check-open-brace", 97 | "check-catch", 98 | "check-else", 99 | "check-finally", 100 | "check-whitespace" 101 | ], 102 | "semicolon": true, 103 | "switch-default": true, 104 | "triple-equals": [ 105 | true, 106 | "allow-null-check" 107 | ], 108 | "typedef-whitespace": [ 109 | true, 110 | { 111 | "call-signature": "nospace", 112 | "index-signature": "nospace", 113 | "parameter": "nospace", 114 | "property-declaration": "nospace", 115 | "variable-declaration": "nospace" 116 | } 117 | ], 118 | "variable-name": false, 119 | "whitespace": [ 120 | true, 121 | "check-branch", 122 | "check-decl", 123 | "check-operator", 124 | "check-separator", 125 | "check-type" 126 | ] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /w3tec-divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeselaGen/typeorm-backend/2456e909f3a4b44d9be65e0a05d0eec1a23705b9/w3tec-divider.png -------------------------------------------------------------------------------- /w3tec-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeselaGen/typeorm-backend/2456e909f3a4b44d9be65e0a05d0eec1a23705b9/w3tec-logo.png --------------------------------------------------------------------------------