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