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