├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .graphqlconfig ├── .node-version ├── .prettierrc.json ├── .vscode └── extensions.json ├── Dockerfile ├── Dockerfile.alpine ├── LICENSE ├── README.md ├── docker-compose.db.yml ├── docker-compose.migrate.yml ├── docker-compose.yml ├── graphql ├── auth.graphql ├── post.graphql └── user.graphql ├── nest-cli.json ├── package-lock.json ├── package.json ├── prisma ├── dbml │ └── schema.dbml ├── migrations │ ├── 20220118074231_init │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seed.ts ├── run.sh ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.resolver.spec.ts ├── app.resolver.ts ├── app.service.ts ├── auth │ ├── auth.module.ts │ ├── auth.resolver.ts │ ├── auth.service.ts │ ├── dto │ │ ├── jwt.dto.ts │ │ ├── login.input.ts │ │ ├── refresh-token.input.ts │ │ └── signup.input.ts │ ├── gql-auth.guard.ts │ ├── jwt.strategy.ts │ ├── models │ │ ├── auth.model.ts │ │ └── token.model.ts │ ├── password.service.spec.ts │ └── password.service.ts ├── common │ ├── configs │ │ ├── config.interface.ts │ │ └── config.ts │ ├── decorators │ │ └── user.decorator.ts │ ├── models │ │ └── base.model.ts │ ├── order │ │ ├── order-direction.ts │ │ └── order.ts │ └── pagination │ │ ├── page-info.model.ts │ │ ├── pagination.args.ts │ │ └── pagination.ts ├── gql-config.service.ts ├── main.ts ├── metadata.ts ├── posts │ ├── args │ │ ├── post-id.args.ts │ │ └── user-id.args.ts │ ├── dto │ │ ├── createPost.input.ts │ │ └── post-order.input.ts │ ├── models │ │ ├── post-connection.model.ts │ │ └── post.model.ts │ ├── posts.module.ts │ └── posts.resolver.ts ├── schema.graphql └── users │ ├── dto │ ├── change-password.input.ts │ └── update-user.input.ts │ ├── models │ └── user.model.ts │ ├── users.module.ts │ ├── users.resolver.ts │ └── users.service.ts ├── test ├── app.e2e-spec.ts ├── app.resolver.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules/ 3 | npm-debug.log 4 | dist/ 5 | graphql/ 6 | test/ -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # POSTGRES 2 | POSTGRES_USER=prisma 3 | POSTGRES_PASSWORD=topsecret 4 | POSTGRES_DB=blog 5 | 6 | # Nest run locally 7 | DB_HOST=localhost 8 | # Nest run in docker, change host to database container name 9 | # DB_HOST=postgres 10 | DB_PORT=5432 11 | DB_SCHEMA=blog 12 | 13 | # Prisma database connection 14 | DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}&sslmode=prefer 15 | 16 | # Nest 17 | PORT=3000 18 | 19 | # Security 20 | JWT_ACCESS_SECRET=nestjsPrismaAccessSecret 21 | JWT_REFRESH_SECRET=nestjsPrismaRefreshSecret 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16, 18] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm install 25 | - run: npm run lint 26 | - run: npm run build 27 | - run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "nest-prisma-starter": { 4 | "schemaPath": "src/schema.graphql", 5 | "extensions": { 6 | "endpoints": { 7 | "dev": "http://localhost:3000/graphql" 8 | } 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18.16.1 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "angular.ng-template", 4 | "ms-vscode.vscode-typescript-tslint-plugin", 5 | "esbenp.prettier-vscode", 6 | "graphql.vscode-graphql", 7 | "prisma.prisma" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 AS builder 2 | 3 | # Create app directory 4 | WORKDIR /app 5 | 6 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 7 | COPY package*.json ./ 8 | COPY prisma ./prisma/ 9 | 10 | # Install app dependencies 11 | RUN npm install 12 | 13 | COPY . . 14 | 15 | RUN npm run build 16 | 17 | FROM node:16 18 | 19 | WORKDIR /app 20 | 21 | COPY --from=builder /app/node_modules ./node_modules 22 | COPY --from=builder /app/package*.json ./ 23 | COPY --from=builder /app/dist ./dist 24 | 25 | EXPOSE 3000 26 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine AS builder 2 | 3 | # Create app directory 4 | WORKDIR /app 5 | 6 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 7 | COPY package*.json ./ 8 | COPY prisma ./prisma/ 9 | 10 | # Install app dependencies 11 | RUN npm install 12 | 13 | COPY . . 14 | 15 | RUN npm run build 16 | 17 | FROM node:16-alpine 18 | 19 | COPY --from=builder /app/node_modules ./node_modules 20 | COPY --from=builder /app/package*.json ./ 21 | COPY --from=builder /app/dist ./dist 22 | 23 | EXPOSE 3000 24 | CMD [ "npm", "run", "start:prod" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 notiz (Gary Großgarten, Marc Stammerjohann) 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instructions 2 | 3 | Starter template for 😻 [NestJS](https://nestjs.com/) and [Prisma](https://www.prisma.io/). 4 | 5 | > Checkout [NestJS Prisma Schematics](https://github.com/marcjulian/nestjs-prisma) to automatically add Prisma support to your Nest application. 6 | 7 | ## Version 8 | 9 | | Branch |  Nest | Prisma |  Graphql | 10 | | ------------------------------------------------------------------------------------------------------------ | ----- | ---------------------------------------------------- | --------------------------------------------------------------------- | 11 | | main | v9 | [v4](https://github.com/prisma/prisma) | [Code-first](https://docs.nestjs.com/graphql/quick-start#code-first) | 12 | | [nest-8-prisma-3](https://github.com/fivethree-team/nestjs-prisma-starter/tree/nest-8-prisma-3) | v8 | [v3](https://github.com/prisma/prisma) | [Code-first](https://docs.nestjs.com/graphql/quick-start#code-first) | 13 | | [nest-7](https://github.com/fivethree-team/nestjs-prisma-starter/tree/nest-7) | v7 | [v2](https://github.com/prisma/prisma2) | [Code-first](https://docs.nestjs.com/graphql/quick-start#code-first) | 14 | | [nest-6-prisma2-code-first](https://github.com/fivethree-team/nestjs-prisma-starter/tree/nest-6-prisma2-code-first) | v6 | [v2-preview](https://github.com/prisma/prisma2) | [Code-first](https://github.com/19majkel94/type-graphql) | 15 | | [nest-6-code-first](https://github.com/fivethree-team/nestjs-prisma-starter/tree/nest-6-code-first) | v6 | [v1](https://github.com/prisma/prisma) | [Code-first](https://github.com/19majkel94/type-graphql) | 16 | | [nest-6-sdl-first](https://github.com/fivethree-team/nestjs-prisma-starter/tree/nest-6-sdl-first) | v6 | [v1](https://github.com/prisma/prisma) | [SDL First](https://docs.nestjs.com/graphql/quick-start#schema-first) | 17 | | [nest-5](https://github.com/fivethree-team/nestjs-prisma-starter/tree/nest-5) | v5 | [v1](https://github.com/prisma/prisma) | [SDL First](https://docs.nestjs.com/graphql/quick-start#schema-first) | 18 | 19 | ## Features 20 | 21 | - GraphQL w/ [playground](https://github.com/prisma/graphql-playground) 22 | - Code-First w/ [decorators](https://docs.nestjs.com/graphql/quick-start#code-first) 23 | - [Prisma](https://www.prisma.io/) for database modelling, migration and type-safe access (Postgres, MySQL & MongoDB) 24 | - 🔐 JWT authentication w/ [passport-jwt](https://github.com/mikenicholson/passport-jwt) 25 | - REST API docs w/ [Swagger](https://swagger.io/) 26 | 27 | ## Overview 28 | 29 | - [Instructions](#instructions) 30 | - [Features](#features) 31 | - [Overview](#overview) 32 | - [Prisma Setup](#prisma-setup) 33 | - [1. Install Dependencies](#1-install-dependencies) 34 | - [2. PostgreSQL with Docker](#2-PostgreSQL-with-docker) 35 | - [3. Prisma: Prisma Migrate](#3-prisma-prisma-migrate) 36 | - [4. Prisma: Prisma Client JS](#4-prisma-client-js) 37 | - [5. Seed the database data with this script](#5-seed-the-database-data-with-this-script) 38 | - [6. Start NestJS Server](#6-start-nestjs-server) 39 | - [GraphQL Playground](#graphql-playground) 40 | - [Rest Api](#rest-api) 41 | - [Docker](#docker) 42 | - [Schema Development](#schema-development) 43 | - [NestJS - Api Schema](#nestjs---api-schema) 44 | - [Resolver](#resolver) 45 | - [GraphQL Client](#graphql-client) 46 | - [Angular](#angular) 47 | - [Setup](#setup) 48 | - [Queries](#queries) 49 | - [Mutations](#mutations) 50 | - [Subscriptions](#subscriptions) 51 | - [Authentication](#authentication) 52 | 53 | ## Prisma Setup 54 | 55 | ### 1. Install Dependencies 56 | 57 | Install [Nestjs CLI](https://docs.nestjs.com/cli/usages) to start and [generate CRUD resources](https://trilon.io/blog/introducing-cli-generators-crud-api-in-1-minute) 58 | 59 | ```bash 60 | # npm 61 | npm i -g @nestjs/cli 62 | # yarn 63 | yarn add -g @nestjs/cli 64 | ``` 65 | 66 | Install the dependencies for the Nest application: 67 | 68 | ```bash 69 | # npm 70 | npm install 71 | # yarn 72 | yarn install 73 | ``` 74 | 75 | ### 2. PostgreSQL with Docker 76 | 77 | Setup a development PostgreSQL with Docker. Copy [.env.example](./.env.example) and rename to `.env` - `cp .env.example .env` - which sets the required environments for PostgreSQL such as `POSTGRES_USER`, `POSTGRES_PASSWORD` and `POSTGRES_DB`. Update the variables as you wish and select a strong password. 78 | 79 | Start the PostgreSQL database 80 | 81 | ```bash 82 | docker-compose -f docker-compose.db.yml up -d 83 | # or 84 | npm run docker:db 85 | ``` 86 | 87 | ### 3. Prisma Migrate 88 | 89 | [Prisma Migrate](https://github.com/prisma/prisma2/tree/master/docs/prisma-migrate) is used to manage the schema and migration of the database. Prisma datasource requires an environment variable `DATABASE_URL` for the connection to the PostgreSQL database. Prisma reads the `DATABASE_URL` from the root [.env](./.env) file. 90 | 91 | Use Prisma Migrate in your [development environment](https://www.prisma.io/blog/prisma-migrate-preview-b5eno5g08d0b#evolving-the-schema-in-development) to 92 | 93 | 1. Creates `migration.sql` file 94 | 2. Updates Database Schema 95 | 3. Generates Prisma Client 96 | 97 | ```bash 98 | npx prisma migrate dev 99 | # or 100 | npm run migrate:dev 101 | ``` 102 | 103 | If you like to customize your `migration.sql` file run the following command. After making your customizations run `npx prisma migrate dev` to apply it. 104 | 105 | ```bash 106 | npx prisma migrate dev --create-only 107 | # or 108 | npm run migrate:dev:create 109 | ``` 110 | 111 | If you are happy with your database changes you want to deploy those changes to your [production database](https://www.prisma.io/blog/prisma-migrate-preview-b5eno5g08d0b#applying-migrations-in-production-and-other-environments). Use `prisma migrate deploy` to apply all pending migrations, can also be used in CI/CD pipelines as it works without prompts. 112 | 113 | ```bash 114 | npx prisma migrate deploy 115 | # or 116 | npm run migrate:deploy 117 | ``` 118 | 119 | ### 4. Prisma: Prisma Client JS 120 | 121 | [Prisma Client JS](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/api) is a type-safe database client auto-generated based on the data model. 122 | 123 | Generate Prisma Client JS by running 124 | 125 | > **Note**: Every time you update [schema.prisma](prisma/schema.prisma) re-generate Prisma Client JS 126 | 127 | ```bash 128 | npx prisma generate 129 | # or 130 | npm run prisma:generate 131 | ``` 132 | 133 | ### 5. Seed the database data with this script 134 | 135 | Execute the script with this command: 136 | 137 | ```bash 138 | npm run seed 139 | ``` 140 | 141 | ### 6. Start NestJS Server 142 | 143 | Run Nest Server in Development mode: 144 | 145 | ```bash 146 | npm run start 147 | 148 | # watch mode 149 | npm run start:dev 150 | ``` 151 | 152 | Run Nest Server in Production mode: 153 | 154 | ```bash 155 | npm run start:prod 156 | ``` 157 | 158 | GraphQL Playground for the NestJS Server is available here: http://localhost:3000/graphql 159 | 160 | **[⬆ back to top](#overview)** 161 | 162 | ## GraphQL Playground 163 | 164 | Open up the [example GraphQL queries](graphql/auth.graphql) and copy them to the GraphQL Playground. Some queries and mutations are secured by an auth guard. You have to acquire a JWT token from `signup` or `login`. Add the `accessToken`as followed to **HTTP HEADERS** in the playground and replace `YOURTOKEN` here: 165 | 166 | ```json 167 | { 168 | "Authorization": "Bearer YOURTOKEN" 169 | } 170 | ``` 171 | 172 | ## Rest Api 173 | 174 | [RESTful API](http://localhost:3000/api) documentation available with Swagger. 175 | 176 | ## Docker 177 | 178 | Nest server is a Node.js application and it is easily [dockerized](https://nodejs.org/de/docs/guides/nodejs-docker-webapp/). 179 | 180 | See the [Dockerfile](./Dockerfile) on how to build a Docker image of your Nest server. 181 | 182 | Now to build a Docker image of your own Nest server simply run: 183 | 184 | ```bash 185 | # give your docker image a name 186 | docker build -t /nest-prisma-server . 187 | # for example 188 | docker build -t nest-prisma-server . 189 | ``` 190 | 191 | After Docker build your docker image you are ready to start up a docker container running the nest server: 192 | 193 | ```bash 194 | docker run -d -t -p 3000:3000 --env-file .env nest-prisma-server 195 | ``` 196 | 197 | Now open up [localhost:3000](http://localhost:3000) to verify that your nest server is running. 198 | 199 | When you run your NestJS application in a Docker container update your [.env](.env) file 200 | 201 | ```diff 202 | - DB_HOST=localhost 203 | # replace with name of the database container 204 | + DB_HOST=postgres 205 | 206 | # Prisma database connection 207 | + DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}&sslmode=prefer 208 | ``` 209 | 210 | If `DATABASE_URL` is missing in the root `.env` file, which is loaded into the Docker container, the NestJS application will exit with the following error: 211 | 212 | ```bash 213 | (node:19) UnhandledPromiseRejectionWarning: Error: error: Environment variable not found: DATABASE_URL. 214 | --> schema.prisma:3 215 | | 216 | 2 | provider = "postgresql" 217 | 3 | url = env("DATABASE_URL") 218 | ``` 219 | 220 | ### Docker Compose 221 | 222 | You can also setup a the database and Nest application with the docker-compose 223 | 224 | ```bash 225 | # building new NestJS docker image 226 | docker-compose build 227 | # or 228 | npm run docker:build 229 | 230 | # start docker-compose 231 | docker-compose up -d 232 | # or 233 | npm run docker 234 | ``` 235 | 236 | ## Schema Development 237 | 238 | Update the Prisma schema `prisma/schema.prisma` and after that run the following two commands: 239 | 240 | ```bash 241 | npx prisma generate 242 | # or in watch mode 243 | npx prisma generate --watch 244 | # or 245 | npm run prisma:generate 246 | npm run prisma:generate:watch 247 | ``` 248 | 249 | **[⬆ back to top](#overview)** 250 | 251 | ## NestJS - Api Schema 252 | 253 | The [schema.graphql](./src/schema.graphql) is generated with [code first approach](https://docs.nestjs.com/graphql/quick-start#code-first) from the models, resolvers and input classes. 254 | 255 | You can use [class-validator](https://docs.nestjs.com/techniques/validation) to validate your inputs and arguments. 256 | 257 | ### Resolver 258 | 259 | To implement the new query, a new resolver function needs to be added to `users.resolver.ts`. 260 | 261 | ```ts 262 | @Query(returns => User) 263 | async getUser(@Args() args): Promise { 264 | return await this.prisma.client.user(args); 265 | } 266 | ``` 267 | 268 | Restart the NestJS server and this time the Query to fetch a `user` should work. 269 | 270 | **[⬆ back to top](#overview)** 271 | 272 | ## GraphQL Client 273 | 274 | A GraphQL client is necessary to consume the GraphQL api provided by the NestJS Server. 275 | 276 | Checkout [Apollo](https://www.apollographql.com/) a popular GraphQL client which offers several clients for React, Angular, Vue.js, Native iOS, Native Android and more. 277 | 278 | ### Angular 279 | 280 | #### Setup 281 | 282 | To start using [Apollo Angular](https://www.apollographql.com/docs/angular/basics/setup.html) simply run in an Angular and Ionic project: 283 | 284 | ```bash 285 | ng add apollo-angular 286 | ``` 287 | 288 | `HttpLink` from apollo-angular requires the `HttpClient`. Therefore, you need to add the `HttpClientModule` to the `AppModule`: 289 | 290 | ```ts 291 | imports: [BrowserModule, 292 | HttpClientModule, 293 | ..., 294 | GraphQLModule], 295 | ``` 296 | 297 | You can also add the `GraphQLModule` in the `AppModule` to make `Apollo` available in your Angular App. 298 | 299 | You need to set the URL to the NestJS GraphQL Api. Open the file `src/app/graphql.module.ts` and update `uri`: 300 | 301 | ```ts 302 | const uri = 'http://localhost:3000/graphql'; 303 | ``` 304 | 305 | To use Apollo-Angular you can inject `private apollo: Apollo` into the constructor of a page, component or service. 306 | 307 | **[⬆ back to top](#overview)** 308 | 309 | #### Queries 310 | 311 | To execute a query you can use: 312 | 313 | ```ts 314 | this.apollo.query({query: YOUR_QUERY}); 315 | 316 | # or 317 | 318 | this.apollo.watchQuery({ 319 | query: YOUR_QUERY 320 | }).valueChanges; 321 | ``` 322 | 323 | Here is an example how to fetch your profile from the NestJS GraphQL Api: 324 | 325 | ```ts 326 | const CurrentUserProfile = gql` 327 | query CurrentUserProfile { 328 | me { 329 | id 330 | email 331 | name 332 | } 333 | } 334 | `; 335 | 336 | @Component({ 337 | selector: 'app-home', 338 | templateUrl: 'home.page.html', 339 | styleUrls: ['home.page.scss'], 340 | }) 341 | export class HomePage implements OnInit { 342 | data: Observable; 343 | 344 | constructor(private apollo: Apollo) {} 345 | 346 | ngOnInit() { 347 | this.data = this.apollo.watchQuery({ 348 | query: CurrentUserProfile, 349 | }).valueChanges; 350 | } 351 | } 352 | ``` 353 | 354 | Use the `AsyncPipe` and [SelectPipe](https://www.apollographql.com/docs/angular/basics/queries.html#select-pipe) to unwrap the data Observable in the template: 355 | 356 | ```html 357 |
358 |

Me id: {{me.id}}

359 |

Me email: {{me.email}}

360 |

Me name: {{me.name}}

361 |
362 | ``` 363 | 364 | Or unwrap the data using [RxJs](https://www.apollographql.com/docs/angular/basics/queries.html#rxjs). 365 | 366 | This will end up in an `GraphQL error` because `Me` is protected by an `@UseGuards(GqlAuthGuard)` and requires an `Bearer TOKEN`. 367 | Please refer to the [Authentication](#authentication) section. 368 | 369 | **[⬆ back to top](#overview)** 370 | 371 | #### Mutations 372 | 373 | To execute a mutation you can use: 374 | 375 | ```ts 376 | this.apollo.mutate({ 377 | mutation: YOUR_MUTATION, 378 | }); 379 | ``` 380 | 381 | Here is an example how to login into your profile using the `login` Mutation: 382 | 383 | ```ts 384 | const Login = gql` 385 | mutation Login { 386 | login(email: "test@example.com", password: "pizzaHawaii") { 387 | token 388 | user { 389 | id 390 | email 391 | name 392 | } 393 | } 394 | } 395 | `; 396 | 397 | @Component({ 398 | selector: 'app-home', 399 | templateUrl: 'home.page.html', 400 | styleUrls: ['home.page.scss'], 401 | }) 402 | export class HomePage implements OnInit { 403 | data: Observable; 404 | 405 | constructor(private apollo: Apollo) {} 406 | 407 | ngOnInit() { 408 | this.data = this.apollo.mutate({ 409 | mutation: Login, 410 | }); 411 | } 412 | } 413 | ``` 414 | 415 | **[⬆ back to top](#overview)** 416 | 417 | #### Subscriptions 418 | 419 | To execute a subscription you can use: 420 | 421 | ```ts 422 | this.apollo.subscribe({ 423 | query: YOUR_SUBSCRIPTION_QUERY, 424 | }); 425 | ``` 426 | 427 | **[⬆ back to top](#overview)** 428 | 429 | #### Authentication 430 | 431 | To authenticate your requests you have to add your `TOKEN` you receive on `signup` and `login` [mutation](#mutations) to each request which is protected by the `@UseGuards(GqlAuthGuard)`. 432 | 433 | Because the apollo client is using `HttpClient` under the hood you are able to simply use an `Interceptor` to add your token to the requests. 434 | 435 | Create the following class: 436 | 437 | ```ts 438 | import { Injectable } from '@angular/core'; 439 | import { 440 | HttpEvent, 441 | HttpInterceptor, 442 | HttpHandler, 443 | HttpRequest, 444 | } from '@angular/common/http'; 445 | import { Observable } from 'rxjs'; 446 | 447 | @Injectable() 448 | export class TokenInterceptor implements HttpInterceptor { 449 | constructor() {} 450 | 451 | intercept( 452 | req: HttpRequest, 453 | next: HttpHandler 454 | ): Observable> { 455 | const token = 'YOUR_TOKEN'; // get from local storage 456 | if (token !== undefined) { 457 | req = req.clone({ 458 | setHeaders: { 459 | Authorization: `Bearer ${token}`, 460 | }, 461 | }); 462 | } 463 | 464 | return next.handle(req); 465 | } 466 | } 467 | ``` 468 | 469 | Add the Interceptor to the `AppModule` providers like this: 470 | 471 | ```ts 472 | providers: [ 473 | ... 474 | { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }, 475 | ... 476 | ] 477 | ``` 478 | 479 | After you configured the Interceptor and retrieved the `TOKEN` from storage your request will succeed on resolvers with `@UseGuards(GqlAuthGuard)`. 480 | 481 | **[⬆ back to top](#overview)** 482 | -------------------------------------------------------------------------------- /docker-compose.db.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | postgres: 4 | image: postgres:15 5 | container_name: postgres 6 | restart: always 7 | ports: 8 | - '5432:5432' 9 | env_file: 10 | - .env 11 | volumes: 12 | - postgres:/var/lib/postgresql/data 13 | 14 | volumes: 15 | postgres: 16 | name: nest-db 17 | -------------------------------------------------------------------------------- /docker-compose.migrate.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | prisma-migrate: 4 | container_name: prisma-migrate 5 | build: 6 | context: prisma 7 | dockerfile: Dockerfile 8 | environment: 9 | DATABASE_URL: ${DATABASE_URL} 10 | depends_on: 11 | - postgres 12 | 13 | postgres: 14 | image: postgres:15 15 | container_name: postgres 16 | restart: always 17 | ports: 18 | - '5432:5432' 19 | env_file: 20 | - .env 21 | volumes: 22 | - postgres:/var/lib/postgresql/data 23 | 24 | volumes: 25 | postgres: 26 | name: nest-db 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | nest-api: 4 | container_name: nest-api 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - '3000:3000' 10 | depends_on: 11 | - postgres 12 | env_file: 13 | - .env 14 | 15 | postgres: 16 | image: postgres:15 17 | container_name: postgres 18 | restart: always 19 | ports: 20 | - '5432:5432' 21 | env_file: 22 | - .env 23 | volumes: 24 | - postgres:/var/lib/postgresql/data 25 | 26 | volumes: 27 | postgres: 28 | name: nest-db 29 | -------------------------------------------------------------------------------- /graphql/auth.graphql: -------------------------------------------------------------------------------- 1 | mutation Admin { 2 | login(data: { email: "bart@simpson.com", password: "secret42" }) { 3 | ...AuthTokens 4 | } 5 | } 6 | 7 | mutation User { 8 | login(data: { email: "lisa@simpson.com", password: "secret42" }) { 9 | ...AuthTokens 10 | } 11 | } 12 | 13 | mutation AuthUser { 14 | signup(data: { email: "bart@simpson.com", password: "secret42" }) { 15 | ...AuthTokens 16 | } 17 | } 18 | 19 | mutation RefreshToken { 20 | refreshToken(token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja3lqdDZrdmkwMDIxcHdldmM0OW8zYTAyIiwiaWF0IjoxNjQ0ODMyNzYyLCJleHAiOjE2NDU0Mzc1NjJ9.eSD3zIsVBNS1pTRtYjjYrCWUYBcUL5ZeCpPTDXEc68Y") { 21 | ...TokenData 22 | } 23 | } 24 | 25 | fragment UserData on User { 26 | id 27 | email 28 | } 29 | 30 | fragment AuthTokens on Auth { 31 | accessToken 32 | refreshToken 33 | user { 34 | ...UserData 35 | } 36 | } 37 | 38 | fragment TokenData on Token { 39 | accessToken 40 | refreshToken 41 | } 42 | -------------------------------------------------------------------------------- /graphql/post.graphql: -------------------------------------------------------------------------------- 1 | query UserPosts { 2 | userPosts(userId: "USER_ID") { 3 | ...PostData 4 | } 5 | } 6 | 7 | query PublishedPostsConnection { 8 | publishedPosts(first: 20, orderBy: { field: title, direction: desc }) { 9 | totalCount 10 | edges { 11 | cursor 12 | node { 13 | ...PostData 14 | author { 15 | ...UserData 16 | } 17 | } 18 | } 19 | pageInfo { 20 | startCursor 21 | endCursor 22 | hasNextPage 23 | hasPreviousPage 24 | } 25 | } 26 | } 27 | 28 | mutation CreatePost { 29 | createPost(data: { content: "Hello", title: "New Post" }) { 30 | ...PostData 31 | } 32 | } 33 | 34 | subscription SubscriptionPost { 35 | postCreated { 36 | ...PostData 37 | } 38 | } 39 | 40 | fragment PostData on Post { 41 | id 42 | createdAt 43 | updatedAt 44 | published 45 | title 46 | content 47 | } 48 | 49 | fragment UserData on User { 50 | id 51 | email 52 | firstname 53 | lastname 54 | role 55 | } 56 | -------------------------------------------------------------------------------- /graphql/user.graphql: -------------------------------------------------------------------------------- 1 | query Me { 2 | me { 3 | ...UserData 4 | } 5 | } 6 | 7 | mutation UpdateUser { 8 | updateUser(data: { firstname: "Bart", lastname: "Simpson" }) { 9 | ...UserData 10 | } 11 | } 12 | 13 | mutation ChangePassword { 14 | changePassword(data: { oldPassword: "secret42", newPassword: "secret42" }) { 15 | ...UserData 16 | } 17 | } 18 | 19 | fragment UserData on User { 20 | id 21 | email 22 | firstname 23 | lastname 24 | role 25 | } 26 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "builder": "swc", 8 | "typeCheck": true, 9 | "plugins": ["@nestjs/swagger/plugin", "@nestjs/graphql/plugin"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-prisma-client-starter", 3 | "version": "0.0.1", 4 | "description": "NestJS Prisma Client Starter Project", 5 | "author": "Marc Stammerjohann", 6 | "license": "MIT", 7 | "keywords": [ 8 | "nestjs", 9 | "prisma", 10 | "prisma client", 11 | "typescript", 12 | "passport", 13 | "graphql" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/fivethree-team/nestjs-prisma-client-example.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/fivethree-team/nestjs-prisma-client-example/issues" 21 | }, 22 | "scripts": { 23 | "build": "nest build", 24 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 25 | "start": "nest start", 26 | "start:dev": "nest start --watch", 27 | "start:debug": "nest start --debug --watch", 28 | "start:prod": "node dist/main", 29 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 30 | "migrate:dev": "prisma migrate dev --preview-feature", 31 | "migrate:dev:create": "prisma migrate dev --create-only --preview-feature", 32 | "migrate:reset": "prisma migrate reset --preview-feature", 33 | "migrate:deploy": "npx prisma migrate deploy --preview-feature", 34 | "migrate:status": "npx prisma migrate status --preview-feature", 35 | "migrate:resolve": "npx prisma migrate resolve --preview-feature", 36 | "prisma:studio": "npx prisma studio", 37 | "prisma:generate": "npx prisma generate", 38 | "prisma:generate:watch": "npx prisma generate --watch", 39 | "test": "jest", 40 | "test:watch": "jest --watch", 41 | "test:cov": "jest --coverage", 42 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 43 | "test:e2e": "jest --config ./test/jest-e2e.json", 44 | "start:db": "npm run migrate:up && npm run prisma:generate && npm run seed", 45 | "seed": "prisma db seed", 46 | "postinstall": "npm run prisma:generate", 47 | "docker:migrate": "docker-compose -f docker-compose.migrate.yml up -d", 48 | "docker:db": "docker-compose -f docker-compose.db.yml up -d", 49 | "docker:seed": "docker exec -it nest-api npm run seed", 50 | "docker": "docker-compose up -d", 51 | "docker:build": "docker-compose build" 52 | }, 53 | "dependencies": { 54 | "@apollo/server": "^4.7.5", 55 | "@devoxa/prisma-relay-cursor-connection": "2.2.3", 56 | "@nestjs/apollo": "12.0.7", 57 | "@nestjs/common": "10.1.0", 58 | "@nestjs/config": "3.0.0", 59 | "@nestjs/core": "10.1.0", 60 | "@nestjs/graphql": "12.0.8", 61 | "@nestjs/jwt": "10.1.0", 62 | "@nestjs/passport": "10.0.0", 63 | "@nestjs/platform-express": "10.1.0", 64 | "@nestjs/swagger": "7.1.2", 65 | "@prisma/client": "^5.0.0", 66 | "bcrypt": "5.1.0", 67 | "class-transformer": "0.5.1", 68 | "class-validator": "0.13.2", 69 | "graphql": "16.7.1", 70 | "graphql-scalars": "1.22.2", 71 | "graphql-subscriptions": "2.0.0", 72 | "nestjs-prisma": "0.22.0-dev.0", 73 | "passport": "0.6.0", 74 | "passport-jwt": "4.0.1", 75 | "reflect-metadata": "0.1.13", 76 | "rxjs": "7.8.1" 77 | }, 78 | "devDependencies": { 79 | "@nestjs/cli": "10.1.10", 80 | "@nestjs/schematics": "10.0.1", 81 | "@nestjs/testing": "10.1.0", 82 | "@swc/cli": "^0.1.62", 83 | "@swc/core": "^1.3.70", 84 | "@types/bcrypt": "5.0.0", 85 | "@types/chance": "1.1.3", 86 | "@types/express": "4.17.17", 87 | "@types/jest": "^29.5.3", 88 | "@types/node": "^20.3.1", 89 | "@types/supertest": "2.0.12", 90 | "@typescript-eslint/eslint-plugin": "^5.59.11", 91 | "@typescript-eslint/parser": "^5.59.11", 92 | "chance": "1.1.11", 93 | "eslint": "^8.42.0", 94 | "eslint-config-prettier": "^8.8.0", 95 | "eslint-plugin-prettier": "^4.2.1", 96 | "jest": "29.6.1", 97 | "prettier": "2.8.8", 98 | "prisma": "5.0.0", 99 | "prisma-dbml-generator": "0.11.0-dev.0", 100 | "supertest": "6.3.3", 101 | "ts-jest": "29.1.1", 102 | "ts-loader": "9.4.4", 103 | "ts-node": "10.9.1", 104 | "tsconfig-paths": "4.2.0", 105 | "typescript": "^5.1.6" 106 | }, 107 | "jest": { 108 | "moduleFileExtensions": [ 109 | "js", 110 | "json", 111 | "ts" 112 | ], 113 | "rootDir": "src", 114 | "testRegex": ".*\\.spec\\.ts$", 115 | "transform": { 116 | "^.+\\.(t|j)s$": "ts-jest" 117 | }, 118 | "collectCoverageFrom": [ 119 | "**/*.(t|j)s" 120 | ], 121 | "coverageDirectory": "../coverage", 122 | "testEnvironment": "node" 123 | }, 124 | "prisma": { 125 | "seed": "ts-node prisma/seed.ts" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /prisma/dbml/schema.dbml: -------------------------------------------------------------------------------- 1 | //// ------------------------------------------------------ 2 | //// THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | //// ------------------------------------------------------ 4 | 5 | Table User { 6 | id String [pk] 7 | createdAt DateTime [default: `now()`, not null] 8 | updatedAt DateTime [not null] 9 | email String [unique, not null] 10 | password String [not null] 11 | firstname String 12 | lastname String 13 | posts Post [not null] 14 | role Role [not null] 15 | } 16 | 17 | Table Post { 18 | id String [pk] 19 | createdAt DateTime [default: `now()`, not null] 20 | updatedAt DateTime [not null] 21 | published Boolean [not null] 22 | title String [not null] 23 | content String 24 | author User 25 | authorId String 26 | } 27 | 28 | Enum Role { 29 | ADMIN 30 | USER 31 | } 32 | 33 | Ref: Post.authorId > User.id -------------------------------------------------------------------------------- /prisma/migrations/20220118074231_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "User" ( 6 | "id" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL, 9 | "email" TEXT NOT NULL, 10 | "password" TEXT NOT NULL, 11 | "firstname" TEXT, 12 | "lastname" TEXT, 13 | "role" "Role" NOT NULL, 14 | 15 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- CreateTable 19 | CREATE TABLE "Post" ( 20 | "id" TEXT NOT NULL, 21 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 22 | "updatedAt" TIMESTAMP(3) NOT NULL, 23 | "published" BOOLEAN NOT NULL, 24 | "title" TEXT NOT NULL, 25 | "content" TEXT, 26 | "authorId" TEXT, 27 | 28 | CONSTRAINT "Post_pkey" PRIMARY KEY ("id") 29 | ); 30 | 31 | -- CreateIndex 32 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 36 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | // previewFeatures = [] 9 | } 10 | 11 | generator dbml { 12 | provider = "prisma-dbml-generator" 13 | } 14 | 15 | model User { 16 | id String @id @default(cuid()) 17 | createdAt DateTime @default(now()) 18 | updatedAt DateTime @updatedAt 19 | email String @unique 20 | password String 21 | firstname String? 22 | lastname String? 23 | posts Post[] 24 | role Role 25 | } 26 | 27 | model Post { 28 | id String @id @default(cuid()) 29 | createdAt DateTime @default(now()) 30 | updatedAt DateTime @updatedAt 31 | published Boolean 32 | title String 33 | content String? 34 | author User? @relation(fields: [authorId], references: [id]) 35 | authorId String? 36 | } 37 | 38 | enum Role { 39 | ADMIN 40 | USER 41 | } 42 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | async function main() { 6 | await prisma.user.deleteMany(); 7 | await prisma.post.deleteMany(); 8 | 9 | console.log('Seeding...'); 10 | 11 | const user1 = await prisma.user.create({ 12 | data: { 13 | email: 'lisa@simpson.com', 14 | firstname: 'Lisa', 15 | lastname: 'Simpson', 16 | password: '$2b$10$EpRnTzVlqHNP0.fUbXUwSOyuiXe/QLSUG6xNekdHgTGmrpHEfIoxm', // secret42 17 | role: 'USER', 18 | posts: { 19 | create: { 20 | title: 'Join us for Prisma Day 2019 in Berlin', 21 | content: 'https://www.prisma.io/day/', 22 | published: true, 23 | }, 24 | }, 25 | }, 26 | }); 27 | const user2 = await prisma.user.create({ 28 | data: { 29 | email: 'bart@simpson.com', 30 | firstname: 'Bart', 31 | lastname: 'Simpson', 32 | role: 'ADMIN', 33 | password: '$2b$10$EpRnTzVlqHNP0.fUbXUwSOyuiXe/QLSUG6xNekdHgTGmrpHEfIoxm', // secret42 34 | posts: { 35 | create: [ 36 | { 37 | title: 'Subscribe to GraphQL Weekly for community news', 38 | content: 'https://graphqlweekly.com/', 39 | published: true, 40 | }, 41 | { 42 | title: 'Follow Prisma on Twitter', 43 | content: 'https://twitter.com/prisma', 44 | published: false, 45 | }, 46 | ], 47 | }, 48 | }, 49 | }); 50 | 51 | console.log({ user1, user2 }); 52 | } 53 | 54 | main() 55 | .catch((e) => console.error(e)) 56 | .finally(async () => { 57 | await prisma.$disconnect(); 58 | }); 59 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | docker build -t nest-prisma-server . 2 | docker run -d -t -p 3000:3000 nest-prisma-server -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { Chance } from 'chance'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | const chance = new Chance(); 7 | 8 | describe('AppController', () => { 9 | let appController: AppController; 10 | 11 | beforeEach(async () => { 12 | const app: TestingModule = await Test.createTestingModule({ 13 | controllers: [AppController], 14 | providers: [AppService], 15 | }).compile(); 16 | 17 | appController = app.get(AppController); 18 | }); 19 | 20 | describe('root', () => { 21 | it('should return "Hello World!"', () => { 22 | expect(appController.getHello()).toBe('Hello World!'); 23 | }); 24 | }); 25 | describe('hello/:name', () => { 26 | it('should return "Hello ${name}!"', () => { 27 | const name = chance.name(); 28 | expect(appController.getHelloName(name)).toBe(`Hello ${name}!`); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | 13 | @Get('hello/:name') 14 | getHelloName(@Param('name') name: string): string { 15 | return this.appService.getHelloName(name); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLModule } from '@nestjs/graphql'; 2 | import { Logger, Module } from '@nestjs/common'; 3 | import { ConfigModule } from '@nestjs/config'; 4 | import { PrismaModule, loggingMiddleware } from 'nestjs-prisma'; 5 | import { AppController } from './app.controller'; 6 | import { AppService } from './app.service'; 7 | import { AppResolver } from './app.resolver'; 8 | import { AuthModule } from './auth/auth.module'; 9 | import { UsersModule } from './users/users.module'; 10 | import { PostsModule } from './posts/posts.module'; 11 | import config from './common/configs/config'; 12 | import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; 13 | import { GqlConfigService } from './gql-config.service'; 14 | 15 | @Module({ 16 | imports: [ 17 | ConfigModule.forRoot({ isGlobal: true, load: [config] }), 18 | PrismaModule.forRoot({ 19 | isGlobal: true, 20 | prismaServiceOptions: { 21 | middlewares: [ 22 | // configure your prisma middleware 23 | loggingMiddleware({ 24 | logger: new Logger('PrismaMiddleware'), 25 | logLevel: 'log', 26 | }), 27 | ], 28 | }, 29 | }), 30 | 31 | GraphQLModule.forRootAsync({ 32 | driver: ApolloDriver, 33 | useClass: GqlConfigService, 34 | }), 35 | 36 | AuthModule, 37 | UsersModule, 38 | PostsModule, 39 | ], 40 | controllers: [AppController], 41 | providers: [AppService, AppResolver], 42 | }) 43 | export class AppModule {} 44 | -------------------------------------------------------------------------------- /src/app.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { Chance } from 'chance'; 3 | import { AppResolver } from './app.resolver'; 4 | import { AppService } from './app.service'; 5 | 6 | const chance = new Chance(); 7 | 8 | describe('AppResolver', () => { 9 | let appResolver: AppResolver; 10 | 11 | beforeEach(async () => { 12 | const app: TestingModule = await Test.createTestingModule({ 13 | providers: [AppResolver, AppService], 14 | }).compile(); 15 | 16 | appResolver = app.get(AppResolver); 17 | }); 18 | 19 | describe('helloWorld', () => { 20 | it('should return "Hello World!"', () => { 21 | expect(appResolver.helloWorld()).toBe('Hello World!'); 22 | }); 23 | }); 24 | describe('hello', () => { 25 | it('should return "Hello ${name}!"', () => { 26 | const name = chance.name(); 27 | expect(appResolver.hello(name)).toBe(`Hello ${name}!`); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/app.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Args } from '@nestjs/graphql'; 2 | 3 | @Resolver() 4 | export class AppResolver { 5 | @Query(() => String) 6 | helloWorld(): string { 7 | return 'Hello World!'; 8 | } 9 | @Query(() => String) 10 | hello(@Args('name') name: string): string { 11 | return `Hello ${name}!`; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | 9 | getHelloName(name: string): string { 10 | return `Hello ${name}!`; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { PasswordService } from './password.service'; 6 | import { GqlAuthGuard } from './gql-auth.guard'; 7 | import { AuthService } from './auth.service'; 8 | import { AuthResolver } from './auth.resolver'; 9 | import { JwtStrategy } from './jwt.strategy'; 10 | import { SecurityConfig } from '../common/configs/config.interface'; 11 | 12 | @Module({ 13 | imports: [ 14 | PassportModule.register({ defaultStrategy: 'jwt' }), 15 | JwtModule.registerAsync({ 16 | useFactory: async (configService: ConfigService) => { 17 | const securityConfig = configService.get('security'); 18 | return { 19 | secret: configService.get('JWT_ACCESS_SECRET'), 20 | signOptions: { 21 | expiresIn: securityConfig.expiresIn, 22 | }, 23 | }; 24 | }, 25 | inject: [ConfigService], 26 | }), 27 | ], 28 | providers: [ 29 | AuthService, 30 | AuthResolver, 31 | JwtStrategy, 32 | GqlAuthGuard, 33 | PasswordService, 34 | ], 35 | exports: [GqlAuthGuard], 36 | }) 37 | export class AuthModule {} 38 | -------------------------------------------------------------------------------- /src/auth/auth.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Mutation, 4 | Args, 5 | Parent, 6 | ResolveField, 7 | } from '@nestjs/graphql'; 8 | import { AuthService } from './auth.service'; 9 | import { Auth } from './models/auth.model'; 10 | import { Token } from './models/token.model'; 11 | import { LoginInput } from './dto/login.input'; 12 | import { SignupInput } from './dto/signup.input'; 13 | import { RefreshTokenInput } from './dto/refresh-token.input'; 14 | import { User } from '../users/models/user.model'; 15 | 16 | @Resolver(() => Auth) 17 | export class AuthResolver { 18 | constructor(private readonly auth: AuthService) {} 19 | 20 | @Mutation(() => Auth) 21 | async signup(@Args('data') data: SignupInput) { 22 | data.email = data.email.toLowerCase(); 23 | const { accessToken, refreshToken } = await this.auth.createUser(data); 24 | return { 25 | accessToken, 26 | refreshToken, 27 | }; 28 | } 29 | 30 | @Mutation(() => Auth) 31 | async login(@Args('data') { email, password }: LoginInput) { 32 | const { accessToken, refreshToken } = await this.auth.login( 33 | email.toLowerCase(), 34 | password, 35 | ); 36 | 37 | return { 38 | accessToken, 39 | refreshToken, 40 | }; 41 | } 42 | 43 | @Mutation(() => Token) 44 | async refreshToken(@Args() { token }: RefreshTokenInput) { 45 | return this.auth.refreshToken(token); 46 | } 47 | 48 | @ResolveField('user', () => User) 49 | async user(@Parent() auth: Auth) { 50 | return await this.auth.getUserFromToken(auth.accessToken); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { PrismaService } from 'nestjs-prisma'; 2 | import { Prisma, User } from '@prisma/client'; 3 | import { 4 | Injectable, 5 | NotFoundException, 6 | BadRequestException, 7 | ConflictException, 8 | UnauthorizedException, 9 | } from '@nestjs/common'; 10 | import { ConfigService } from '@nestjs/config'; 11 | import { JwtService } from '@nestjs/jwt'; 12 | import { PasswordService } from './password.service'; 13 | import { SignupInput } from './dto/signup.input'; 14 | import { Token } from './models/token.model'; 15 | import { SecurityConfig } from '../common/configs/config.interface'; 16 | 17 | @Injectable() 18 | export class AuthService { 19 | constructor( 20 | private readonly jwtService: JwtService, 21 | private readonly prisma: PrismaService, 22 | private readonly passwordService: PasswordService, 23 | private readonly configService: ConfigService, 24 | ) {} 25 | 26 | async createUser(payload: SignupInput): Promise { 27 | const hashedPassword = await this.passwordService.hashPassword( 28 | payload.password, 29 | ); 30 | 31 | try { 32 | const user = await this.prisma.user.create({ 33 | data: { 34 | ...payload, 35 | password: hashedPassword, 36 | role: 'USER', 37 | }, 38 | }); 39 | 40 | return this.generateTokens({ 41 | userId: user.id, 42 | }); 43 | } catch (e) { 44 | if ( 45 | e instanceof Prisma.PrismaClientKnownRequestError && 46 | e.code === 'P2002' 47 | ) { 48 | throw new ConflictException(`Email ${payload.email} already used.`); 49 | } 50 | throw new Error(e); 51 | } 52 | } 53 | 54 | async login(email: string, password: string): Promise { 55 | const user = await this.prisma.user.findUnique({ where: { email } }); 56 | 57 | if (!user) { 58 | throw new NotFoundException(`No user found for email: ${email}`); 59 | } 60 | 61 | const passwordValid = await this.passwordService.validatePassword( 62 | password, 63 | user.password, 64 | ); 65 | 66 | if (!passwordValid) { 67 | throw new BadRequestException('Invalid password'); 68 | } 69 | 70 | return this.generateTokens({ 71 | userId: user.id, 72 | }); 73 | } 74 | 75 | validateUser(userId: string): Promise { 76 | return this.prisma.user.findUnique({ where: { id: userId } }); 77 | } 78 | 79 | getUserFromToken(token: string): Promise { 80 | const id = this.jwtService.decode(token)['userId']; 81 | return this.prisma.user.findUnique({ where: { id } }); 82 | } 83 | 84 | generateTokens(payload: { userId: string }): Token { 85 | return { 86 | accessToken: this.generateAccessToken(payload), 87 | refreshToken: this.generateRefreshToken(payload), 88 | }; 89 | } 90 | 91 | private generateAccessToken(payload: { userId: string }): string { 92 | return this.jwtService.sign(payload); 93 | } 94 | 95 | private generateRefreshToken(payload: { userId: string }): string { 96 | const securityConfig = this.configService.get('security'); 97 | return this.jwtService.sign(payload, { 98 | secret: this.configService.get('JWT_REFRESH_SECRET'), 99 | expiresIn: securityConfig.refreshIn, 100 | }); 101 | } 102 | 103 | refreshToken(token: string) { 104 | try { 105 | const { userId } = this.jwtService.verify(token, { 106 | secret: this.configService.get('JWT_REFRESH_SECRET'), 107 | }); 108 | 109 | return this.generateTokens({ 110 | userId, 111 | }); 112 | } catch (e) { 113 | throw new UnauthorizedException(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/auth/dto/jwt.dto.ts: -------------------------------------------------------------------------------- 1 | export interface JwtDto { 2 | userId: string; 3 | /** 4 | * Issued at 5 | */ 6 | iat: number; 7 | /** 8 | * Expiration time 9 | */ 10 | exp: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/dto/login.input.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; 2 | import { InputType, Field } from '@nestjs/graphql'; 3 | 4 | @InputType() 5 | export class LoginInput { 6 | @Field() 7 | @IsEmail() 8 | email: string; 9 | 10 | @Field() 11 | @IsNotEmpty() 12 | @MinLength(8) 13 | password: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/auth/dto/refresh-token.input.ts: -------------------------------------------------------------------------------- 1 | import { ArgsType, Field } from '@nestjs/graphql'; 2 | import { IsJWT, IsNotEmpty } from 'class-validator'; 3 | import { GraphQLJWT } from 'graphql-scalars'; 4 | 5 | @ArgsType() 6 | export class RefreshTokenInput { 7 | @IsNotEmpty() 8 | @IsJWT() 9 | @Field(() => GraphQLJWT) 10 | token: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/dto/signup.input.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; 2 | import { InputType, Field } from '@nestjs/graphql'; 3 | 4 | @InputType() 5 | export class SignupInput { 6 | @Field() 7 | @IsEmail() 8 | email: string; 9 | 10 | @Field() 11 | @IsNotEmpty() 12 | @MinLength(8) 13 | password: string; 14 | 15 | @Field({ nullable: true }) 16 | firstname?: string; 17 | 18 | @Field({ nullable: true }) 19 | lastname?: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/auth/gql-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { GqlExecutionContext } from '@nestjs/graphql'; 4 | 5 | @Injectable() 6 | export class GqlAuthGuard extends AuthGuard('jwt') { 7 | getRequest(context: ExecutionContext) { 8 | const ctx = GqlExecutionContext.create(context); 9 | return ctx.getContext().req; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy, ExtractJwt } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { User } from '@prisma/client'; 6 | import { AuthService } from './auth.service'; 7 | import { JwtDto } from './dto/jwt.dto'; 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor( 12 | private readonly authService: AuthService, 13 | readonly configService: ConfigService, 14 | ) { 15 | super({ 16 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 17 | secretOrKey: configService.get('JWT_ACCESS_SECRET'), 18 | }); 19 | } 20 | 21 | async validate(payload: JwtDto): Promise { 22 | const user = await this.authService.validateUser(payload.userId); 23 | if (!user) { 24 | throw new UnauthorizedException(); 25 | } 26 | return user; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/auth/models/auth.model.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType } from '@nestjs/graphql'; 2 | import { User } from '../../users/models/user.model'; 3 | import { Token } from './token.model'; 4 | 5 | @ObjectType() 6 | export class Auth extends Token { 7 | user: User; 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/models/token.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | import { GraphQLJWT } from 'graphql-scalars'; 3 | 4 | @ObjectType() 5 | export class Token { 6 | @Field(() => GraphQLJWT, { description: 'JWT access token' }) 7 | accessToken: string; 8 | 9 | @Field(() => GraphQLJWT, { description: 'JWT refresh token' }) 10 | refreshToken: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/password.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PrismaService } from 'nestjs-prisma'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { PasswordService } from './password.service'; 5 | 6 | describe('PasswordService', () => { 7 | let service: PasswordService; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | providers: [PasswordService, PrismaService, ConfigService], 12 | }).compile(); 13 | 14 | service = module.get(PasswordService); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(service).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/auth/password.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { hash, compare } from 'bcrypt'; 4 | import { SecurityConfig } from '../common/configs/config.interface'; 5 | 6 | @Injectable() 7 | export class PasswordService { 8 | get bcryptSaltRounds(): string | number { 9 | const securityConfig = this.configService.get('security'); 10 | const saltOrRounds = securityConfig.bcryptSaltOrRound; 11 | 12 | return Number.isInteger(Number(saltOrRounds)) 13 | ? Number(saltOrRounds) 14 | : saltOrRounds; 15 | } 16 | 17 | constructor(private configService: ConfigService) {} 18 | 19 | validatePassword(password: string, hashedPassword: string): Promise { 20 | return compare(password, hashedPassword); 21 | } 22 | 23 | hashPassword(password: string): Promise { 24 | return hash(password, this.bcryptSaltRounds); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/common/configs/config.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | nest: NestConfig; 3 | cors: CorsConfig; 4 | swagger: SwaggerConfig; 5 | graphql: GraphqlConfig; 6 | security: SecurityConfig; 7 | } 8 | 9 | export interface NestConfig { 10 | port: number; 11 | } 12 | 13 | export interface CorsConfig { 14 | enabled: boolean; 15 | } 16 | 17 | export interface SwaggerConfig { 18 | enabled: boolean; 19 | title: string; 20 | description: string; 21 | version: string; 22 | path: string; 23 | } 24 | 25 | export interface GraphqlConfig { 26 | playgroundEnabled: boolean; 27 | debug: boolean; 28 | schemaDestination: string; 29 | sortSchema: boolean; 30 | } 31 | 32 | export interface SecurityConfig { 33 | expiresIn: string; 34 | refreshIn: string; 35 | bcryptSaltOrRound: string | number; 36 | } 37 | -------------------------------------------------------------------------------- /src/common/configs/config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from './config.interface'; 2 | 3 | const config: Config = { 4 | nest: { 5 | port: 3000, 6 | }, 7 | cors: { 8 | enabled: true, 9 | }, 10 | swagger: { 11 | enabled: true, 12 | title: 'Nestjs FTW', 13 | description: 'The nestjs API description', 14 | version: '1.5', 15 | path: 'api', 16 | }, 17 | graphql: { 18 | playgroundEnabled: true, 19 | debug: true, 20 | schemaDestination: './src/schema.graphql', 21 | sortSchema: true, 22 | }, 23 | security: { 24 | expiresIn: '2m', 25 | refreshIn: '7d', 26 | bcryptSaltOrRound: 10, 27 | }, 28 | }; 29 | 30 | export default (): Config => config; 31 | -------------------------------------------------------------------------------- /src/common/decorators/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | 4 | export const UserEntity = createParamDecorator( 5 | (data: unknown, ctx: ExecutionContext) => 6 | GqlExecutionContext.create(ctx).getContext().req.user, 7 | ); 8 | -------------------------------------------------------------------------------- /src/common/models/base.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, ID } from '@nestjs/graphql'; 2 | 3 | @ObjectType({ isAbstract: true }) 4 | export abstract class BaseModel { 5 | @Field(() => ID) 6 | id: string; 7 | @Field({ 8 | description: 'Identifies the date and time when the object was created.', 9 | }) 10 | createdAt: Date; 11 | @Field({ 12 | description: 13 | 'Identifies the date and time when the object was last updated.', 14 | }) 15 | updatedAt: Date; 16 | } 17 | -------------------------------------------------------------------------------- /src/common/order/order-direction.ts: -------------------------------------------------------------------------------- 1 | import { registerEnumType } from '@nestjs/graphql'; 2 | 3 | export enum OrderDirection { 4 | // Specifies an ascending order for a given `orderBy` argument. 5 | asc = 'asc', 6 | // Specifies a descending order for a given `orderBy` argument. 7 | desc = 'desc', 8 | } 9 | 10 | registerEnumType(OrderDirection, { 11 | name: 'OrderDirection', 12 | description: 13 | 'Possible directions in which to order a list of items when provided an `orderBy` argument.', 14 | }); 15 | -------------------------------------------------------------------------------- /src/common/order/order.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { OrderDirection } from './order-direction'; 3 | 4 | @InputType({ isAbstract: true }) 5 | export abstract class Order { 6 | @Field(() => OrderDirection) 7 | direction: OrderDirection; 8 | } 9 | -------------------------------------------------------------------------------- /src/common/pagination/page-info.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class PageInfo { 5 | @Field(() => String, { nullable: true }) 6 | endCursor?: string; 7 | 8 | @Field(() => Boolean) 9 | hasNextPage: boolean; 10 | 11 | @Field(() => Boolean) 12 | hasPreviousPage: boolean; 13 | 14 | @Field(() => String, { nullable: true }) 15 | startCursor?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/common/pagination/pagination.args.ts: -------------------------------------------------------------------------------- 1 | import { ArgsType } from '@nestjs/graphql'; 2 | 3 | @ArgsType() 4 | export class PaginationArgs { 5 | skip?: number; 6 | 7 | after?: string; 8 | 9 | before?: string; 10 | 11 | first?: number; 12 | 13 | last?: number; 14 | } 15 | -------------------------------------------------------------------------------- /src/common/pagination/pagination.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, Int } from '@nestjs/graphql'; 2 | import { Type } from '@nestjs/common'; 3 | import { PageInfo } from './page-info.model'; 4 | 5 | export default function Paginated(TItemClass: Type) { 6 | @ObjectType(`${TItemClass.name}Edge`) 7 | abstract class EdgeType { 8 | @Field(() => String) 9 | cursor: string; 10 | 11 | @Field(() => TItemClass) 12 | node: TItem; 13 | } 14 | 15 | // `isAbstract` decorator option is mandatory to prevent registering in schema 16 | @ObjectType({ isAbstract: true }) 17 | abstract class PaginatedType { 18 | @Field(() => [EdgeType], { nullable: true }) 19 | edges: Array; 20 | 21 | // @Field((type) => [TItemClass], { nullable: true }) 22 | // nodes: Array; 23 | 24 | @Field(() => PageInfo) 25 | pageInfo: PageInfo; 26 | 27 | @Field(() => Int) 28 | totalCount: number; 29 | } 30 | return PaginatedType; 31 | } 32 | -------------------------------------------------------------------------------- /src/gql-config.service.ts: -------------------------------------------------------------------------------- 1 | import { GraphqlConfig } from './common/configs/config.interface'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { ApolloDriverConfig } from '@nestjs/apollo'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { GqlOptionsFactory } from '@nestjs/graphql'; 6 | 7 | @Injectable() 8 | export class GqlConfigService implements GqlOptionsFactory { 9 | constructor(private configService: ConfigService) {} 10 | createGqlOptions(): ApolloDriverConfig { 11 | const graphqlConfig = this.configService.get('graphql'); 12 | return { 13 | // schema options 14 | autoSchemaFile: graphqlConfig.schemaDestination || './src/schema.graphql', 15 | sortSchema: graphqlConfig.sortSchema, 16 | buildSchemaOptions: { 17 | numberScalarMode: 'integer', 18 | }, 19 | // subscription 20 | installSubscriptionHandlers: true, 21 | includeStacktraceInErrorResponses: graphqlConfig.debug, 22 | playground: graphqlConfig.playgroundEnabled, 23 | context: ({ req }) => ({ req }), 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { HttpAdapterHost, NestFactory } from '@nestjs/core'; 4 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 5 | import { PrismaClientExceptionFilter } from 'nestjs-prisma'; 6 | import { AppModule } from './app.module'; 7 | import type { 8 | CorsConfig, 9 | NestConfig, 10 | SwaggerConfig, 11 | } from './common/configs/config.interface'; 12 | 13 | async function bootstrap() { 14 | const app = await NestFactory.create(AppModule); 15 | 16 | // Validation 17 | app.useGlobalPipes(new ValidationPipe()); 18 | 19 | // enable shutdown hook 20 | app.enableShutdownHooks(); 21 | 22 | // Prisma Client Exception Filter for unhandled exceptions 23 | const { httpAdapter } = app.get(HttpAdapterHost); 24 | app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter)); 25 | 26 | const configService = app.get(ConfigService); 27 | const nestConfig = configService.get('nest'); 28 | const corsConfig = configService.get('cors'); 29 | const swaggerConfig = configService.get('swagger'); 30 | 31 | // Swagger Api 32 | if (swaggerConfig.enabled) { 33 | const options = new DocumentBuilder() 34 | .setTitle(swaggerConfig.title || 'Nestjs') 35 | .setDescription(swaggerConfig.description || 'The nestjs API description') 36 | .setVersion(swaggerConfig.version || '1.0') 37 | .build(); 38 | const document = SwaggerModule.createDocument(app, options); 39 | 40 | SwaggerModule.setup(swaggerConfig.path || 'api', app, document); 41 | } 42 | 43 | // Cors 44 | if (corsConfig.enabled) { 45 | app.enableCors(); 46 | } 47 | 48 | await app.listen(process.env.PORT || nestConfig.port || 3000); 49 | } 50 | bootstrap(); 51 | -------------------------------------------------------------------------------- /src/metadata.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default async () => { 3 | const t = { 4 | ["./users/models/user.model"]: await import("./users/models/user.model"), 5 | ["./posts/dto/post-order.input"]: await import("./posts/dto/post-order.input") 6 | }; 7 | return { "@nestjs/swagger/plugin": { "models": [], "controllers": [[import("./app.controller"), { "AppController": { "getHello": { type: String }, "getHelloName": { type: String } } }]] }, "@nestjs/graphql/plugin": { "models": [[import("./auth/dto/signup.input"), { "SignupInput": { email: {}, password: {}, firstname: { nullable: true }, lastname: { nullable: true } } }], [import("./auth/models/token.model"), { "Token": { accessToken: {}, refreshToken: {} } }], [import("./common/models/base.model"), { "BaseModel": { id: {}, createdAt: {}, updatedAt: {} } }], [import("./posts/models/post.model"), { "Post": { title: {}, content: { nullable: true }, published: {}, author: { nullable: true } } }], [import("./users/models/user.model"), { "User": { email: {}, firstname: { nullable: true }, lastname: { nullable: true }, role: {}, posts: { nullable: true } } }], [import("./auth/models/auth.model"), { "Auth": { user: { type: () => t["./users/models/user.model"].User } } }], [import("./auth/dto/login.input"), { "LoginInput": { email: {}, password: {} } }], [import("./auth/dto/refresh-token.input"), { "RefreshTokenInput": { token: {} } }], [import("./users/dto/change-password.input"), { "ChangePasswordInput": { oldPassword: {}, newPassword: {} } }], [import("./users/dto/update-user.input"), { "UpdateUserInput": { firstname: { nullable: true }, lastname: { nullable: true } } }], [import("./common/pagination/pagination.args"), { "PaginationArgs": { skip: { nullable: true, type: () => Number }, after: { nullable: true, type: () => String }, before: { nullable: true, type: () => String }, first: { nullable: true, type: () => Number }, last: { nullable: true, type: () => Number } } }], [import("./posts/args/post-id.args"), { "PostIdArgs": { postId: { type: () => String } } }], [import("./posts/args/user-id.args"), { "UserIdArgs": { userId: { type: () => String } } }], [import("./common/pagination/page-info.model"), { "PageInfo": { endCursor: { nullable: true }, hasNextPage: {}, hasPreviousPage: {}, startCursor: { nullable: true } } }], [import("./posts/models/post-connection.model"), { "PostConnection": {} }], [import("./posts/dto/post-order.input"), { "PostOrder": { field: { type: () => t["./posts/dto/post-order.input"].PostOrderField } } }], [import("./posts/dto/createPost.input"), { "CreatePostInput": { content: {}, title: {} } }]] } }; 8 | }; -------------------------------------------------------------------------------- /src/posts/args/post-id.args.ts: -------------------------------------------------------------------------------- 1 | import { ArgsType } from '@nestjs/graphql'; 2 | import { IsNotEmpty } from 'class-validator'; 3 | 4 | @ArgsType() 5 | export class PostIdArgs { 6 | @IsNotEmpty() 7 | postId: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/posts/args/user-id.args.ts: -------------------------------------------------------------------------------- 1 | import { ArgsType } from '@nestjs/graphql'; 2 | import { IsNotEmpty } from 'class-validator'; 3 | 4 | @ArgsType() 5 | export class UserIdArgs { 6 | @IsNotEmpty() 7 | userId: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/posts/dto/createPost.input.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { InputType, Field } from '@nestjs/graphql'; 3 | 4 | @InputType() 5 | export class CreatePostInput { 6 | @Field() 7 | @IsNotEmpty() 8 | content: string; 9 | 10 | @Field() 11 | @IsNotEmpty() 12 | title: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/posts/dto/post-order.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType, registerEnumType } from '@nestjs/graphql'; 2 | import { Order } from '../../common/order/order'; 3 | 4 | export enum PostOrderField { 5 | id = 'id', 6 | createdAt = 'createdAt', 7 | updatedAt = 'updatedAt', 8 | published = 'published', 9 | title = 'title', 10 | content = 'content', 11 | } 12 | 13 | registerEnumType(PostOrderField, { 14 | name: 'PostOrderField', 15 | description: 'Properties by which post connections can be ordered.', 16 | }); 17 | 18 | @InputType() 19 | export class PostOrder extends Order { 20 | @Field(() => PostOrderField) 21 | field: PostOrderField; 22 | } 23 | -------------------------------------------------------------------------------- /src/posts/models/post-connection.model.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType } from '@nestjs/graphql'; 2 | import PaginatedResponse from '../../common/pagination/pagination'; 3 | import { Post } from './post.model'; 4 | 5 | @ObjectType() 6 | export class PostConnection extends PaginatedResponse(Post) {} 7 | -------------------------------------------------------------------------------- /src/posts/models/post.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | import { User } from '../../users/models/user.model'; 3 | import { BaseModel } from '../../common/models/base.model'; 4 | 5 | @ObjectType() 6 | export class Post extends BaseModel { 7 | @Field() 8 | title: string; 9 | 10 | @Field(() => String, { nullable: true }) 11 | content?: string | null; 12 | 13 | @Field(() => Boolean) 14 | published: boolean; 15 | 16 | @Field(() => User, { nullable: true }) 17 | author?: User | null; 18 | } 19 | -------------------------------------------------------------------------------- /src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PostsResolver } from './posts.resolver'; 3 | 4 | @Module({ 5 | imports: [], 6 | providers: [PostsResolver], 7 | }) 8 | export class PostsModule {} 9 | -------------------------------------------------------------------------------- /src/posts/posts.resolver.ts: -------------------------------------------------------------------------------- 1 | import { PrismaService } from 'nestjs-prisma'; 2 | import { 3 | Resolver, 4 | Query, 5 | Parent, 6 | Args, 7 | ResolveField, 8 | Subscription, 9 | Mutation, 10 | } from '@nestjs/graphql'; 11 | import { findManyCursorConnection } from '@devoxa/prisma-relay-cursor-connection'; 12 | import { PubSub } from 'graphql-subscriptions'; 13 | import { UseGuards } from '@nestjs/common'; 14 | import { PaginationArgs } from '../common/pagination/pagination.args'; 15 | import { UserEntity } from '../common/decorators/user.decorator'; 16 | import { User } from '../users/models/user.model'; 17 | import { GqlAuthGuard } from '../auth/gql-auth.guard'; 18 | import { PostIdArgs } from './args/post-id.args'; 19 | import { UserIdArgs } from './args/user-id.args'; 20 | import { Post } from './models/post.model'; 21 | import { PostConnection } from './models/post-connection.model'; 22 | import { PostOrder } from './dto/post-order.input'; 23 | import { CreatePostInput } from './dto/createPost.input'; 24 | 25 | const pubSub = new PubSub(); 26 | 27 | @Resolver(() => Post) 28 | export class PostsResolver { 29 | constructor(private prisma: PrismaService) {} 30 | 31 | @Subscription(() => Post) 32 | postCreated() { 33 | return pubSub.asyncIterator('postCreated'); 34 | } 35 | 36 | @UseGuards(GqlAuthGuard) 37 | @Mutation(() => Post) 38 | async createPost( 39 | @UserEntity() user: User, 40 | @Args('data') data: CreatePostInput, 41 | ) { 42 | const newPost = this.prisma.post.create({ 43 | data: { 44 | published: true, 45 | title: data.title, 46 | content: data.content, 47 | authorId: user.id, 48 | }, 49 | }); 50 | pubSub.publish('postCreated', { postCreated: newPost }); 51 | return newPost; 52 | } 53 | 54 | @Query(() => PostConnection) 55 | async publishedPosts( 56 | @Args() { after, before, first, last }: PaginationArgs, 57 | @Args({ name: 'query', type: () => String, nullable: true }) 58 | query: string, 59 | @Args({ 60 | name: 'orderBy', 61 | type: () => PostOrder, 62 | nullable: true, 63 | }) 64 | orderBy: PostOrder, 65 | ) { 66 | const a = await findManyCursorConnection( 67 | (args) => 68 | this.prisma.post.findMany({ 69 | include: { author: true }, 70 | where: { 71 | published: true, 72 | title: { contains: query || '' }, 73 | }, 74 | orderBy: orderBy ? { [orderBy.field]: orderBy.direction } : undefined, 75 | ...args, 76 | }), 77 | () => 78 | this.prisma.post.count({ 79 | where: { 80 | published: true, 81 | title: { contains: query || '' }, 82 | }, 83 | }), 84 | { first, last, before, after }, 85 | ); 86 | return a; 87 | } 88 | 89 | @Query(() => [Post]) 90 | userPosts(@Args() id: UserIdArgs) { 91 | return this.prisma.user 92 | .findUnique({ where: { id: id.userId } }) 93 | .posts({ where: { published: true } }); 94 | 95 | // or 96 | // return this.prisma.posts.findMany({ 97 | // where: { 98 | // published: true, 99 | // author: { id: id.userId } 100 | // } 101 | // }); 102 | } 103 | 104 | @Query(() => Post) 105 | async post(@Args() id: PostIdArgs) { 106 | return this.prisma.post.findUnique({ where: { id: id.postId } }); 107 | } 108 | 109 | @ResolveField('author', () => User) 110 | async author(@Parent() post: Post) { 111 | return this.prisma.post.findUnique({ where: { id: post.id } }).author(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/schema.graphql: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------ 2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | # ------------------------------------------------------ 4 | 5 | type Auth { 6 | """JWT access token""" 7 | accessToken: JWT! 8 | 9 | """JWT refresh token""" 10 | refreshToken: JWT! 11 | user: User! 12 | } 13 | 14 | input ChangePasswordInput { 15 | newPassword: String! 16 | oldPassword: String! 17 | } 18 | 19 | input CreatePostInput { 20 | content: String! 21 | title: String! 22 | } 23 | 24 | """ 25 | A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. 26 | """ 27 | scalar DateTime 28 | 29 | """ 30 | A field whose value is a JSON Web Token (JWT): https://jwt.io/introduction. 31 | """ 32 | scalar JWT 33 | 34 | input LoginInput { 35 | email: String! 36 | password: String! 37 | } 38 | 39 | type Mutation { 40 | changePassword(data: ChangePasswordInput!): User! 41 | createPost(data: CreatePostInput!): Post! 42 | login(data: LoginInput!): Auth! 43 | refreshToken(token: JWT!): Token! 44 | signup(data: SignupInput!): Auth! 45 | updateUser(data: UpdateUserInput!): User! 46 | } 47 | 48 | """ 49 | Possible directions in which to order a list of items when provided an `orderBy` argument. 50 | """ 51 | enum OrderDirection { 52 | asc 53 | desc 54 | } 55 | 56 | type PageInfo { 57 | endCursor: String 58 | hasNextPage: Boolean! 59 | hasPreviousPage: Boolean! 60 | startCursor: String 61 | } 62 | 63 | type Post { 64 | author: User 65 | content: String 66 | 67 | """Identifies the date and time when the object was created.""" 68 | createdAt: DateTime! 69 | id: ID! 70 | published: Boolean! 71 | title: String! 72 | 73 | """Identifies the date and time when the object was last updated.""" 74 | updatedAt: DateTime! 75 | } 76 | 77 | type PostConnection { 78 | edges: [PostEdge!] 79 | pageInfo: PageInfo! 80 | totalCount: Int! 81 | } 82 | 83 | type PostEdge { 84 | cursor: String! 85 | node: Post! 86 | } 87 | 88 | input PostOrder { 89 | direction: OrderDirection! 90 | } 91 | 92 | type Query { 93 | hello(name: String!): String! 94 | helloWorld: String! 95 | me: User! 96 | post: Post! 97 | publishedPosts(orderBy: PostOrder, query: String): PostConnection! 98 | userPosts: [Post!]! 99 | } 100 | 101 | """User role""" 102 | enum Role { 103 | ADMIN 104 | USER 105 | } 106 | 107 | input SignupInput { 108 | email: String! 109 | firstname: String 110 | lastname: String 111 | password: String! 112 | } 113 | 114 | type Subscription { 115 | postCreated: Post! 116 | } 117 | 118 | type Token { 119 | """JWT access token""" 120 | accessToken: JWT! 121 | 122 | """JWT refresh token""" 123 | refreshToken: JWT! 124 | } 125 | 126 | input UpdateUserInput { 127 | firstname: String 128 | lastname: String 129 | } 130 | 131 | type User { 132 | """Identifies the date and time when the object was created.""" 133 | createdAt: DateTime! 134 | email: String! 135 | firstname: String 136 | id: ID! 137 | lastname: String 138 | posts: [Post!] 139 | role: Role! 140 | 141 | """Identifies the date and time when the object was last updated.""" 142 | updatedAt: DateTime! 143 | } -------------------------------------------------------------------------------- /src/users/dto/change-password.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql'; 2 | import { IsNotEmpty, MinLength } from 'class-validator'; 3 | 4 | @InputType() 5 | export class ChangePasswordInput { 6 | @Field() 7 | @IsNotEmpty() 8 | @MinLength(8) 9 | oldPassword: string; 10 | 11 | @Field() 12 | @IsNotEmpty() 13 | @MinLength(8) 14 | newPassword: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/users/dto/update-user.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql'; 2 | 3 | @InputType() 4 | export class UpdateUserInput { 5 | @Field({ nullable: true }) 6 | firstname?: string; 7 | @Field({ nullable: true }) 8 | lastname?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/users/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { 3 | ObjectType, 4 | registerEnumType, 5 | HideField, 6 | Field, 7 | } from '@nestjs/graphql'; 8 | import { IsEmail } from 'class-validator'; 9 | import { Post } from '../../posts/models/post.model'; 10 | import { BaseModel } from '../../common/models/base.model'; 11 | import { Role } from '@prisma/client'; 12 | 13 | registerEnumType(Role, { 14 | name: 'Role', 15 | description: 'User role', 16 | }); 17 | 18 | @ObjectType() 19 | export class User extends BaseModel { 20 | @Field() 21 | @IsEmail() 22 | email: string; 23 | 24 | @Field(() => String, { nullable: true }) 25 | firstname?: string; 26 | 27 | @Field(() => String, { nullable: true }) 28 | lastname?: string; 29 | 30 | @Field(() => Role) 31 | role: Role; 32 | 33 | @Field(() => [Post], { nullable: true }) 34 | posts?: [Post] | null; 35 | 36 | @HideField() 37 | password: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersResolver } from './users.resolver'; 3 | import { UsersService } from './users.service'; 4 | import { PasswordService } from '../auth/password.service'; 5 | 6 | @Module({ 7 | imports: [], 8 | providers: [UsersResolver, UsersService, PasswordService], 9 | }) 10 | export class UsersModule {} 11 | -------------------------------------------------------------------------------- /src/users/users.resolver.ts: -------------------------------------------------------------------------------- 1 | import { PrismaService } from 'nestjs-prisma'; 2 | import { 3 | Resolver, 4 | Query, 5 | Parent, 6 | Mutation, 7 | Args, 8 | ResolveField, 9 | } from '@nestjs/graphql'; 10 | import { UseGuards } from '@nestjs/common'; 11 | import { UserEntity } from '../common/decorators/user.decorator'; 12 | import { GqlAuthGuard } from '../auth/gql-auth.guard'; 13 | import { UsersService } from './users.service'; 14 | import { User } from './models/user.model'; 15 | import { ChangePasswordInput } from './dto/change-password.input'; 16 | import { UpdateUserInput } from './dto/update-user.input'; 17 | 18 | @Resolver(() => User) 19 | @UseGuards(GqlAuthGuard) 20 | export class UsersResolver { 21 | constructor( 22 | private usersService: UsersService, 23 | private prisma: PrismaService, 24 | ) {} 25 | 26 | @Query(() => User) 27 | async me(@UserEntity() user: User): Promise { 28 | return user; 29 | } 30 | 31 | @UseGuards(GqlAuthGuard) 32 | @Mutation(() => User) 33 | async updateUser( 34 | @UserEntity() user: User, 35 | @Args('data') newUserData: UpdateUserInput, 36 | ) { 37 | return this.usersService.updateUser(user.id, newUserData); 38 | } 39 | 40 | @UseGuards(GqlAuthGuard) 41 | @Mutation(() => User) 42 | async changePassword( 43 | @UserEntity() user: User, 44 | @Args('data') changePassword: ChangePasswordInput, 45 | ) { 46 | return this.usersService.changePassword( 47 | user.id, 48 | user.password, 49 | changePassword, 50 | ); 51 | } 52 | 53 | @ResolveField('posts') 54 | posts(@Parent() author: User) { 55 | return this.prisma.user.findUnique({ where: { id: author.id } }).posts(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { PrismaService } from 'nestjs-prisma'; 2 | import { Injectable, BadRequestException } from '@nestjs/common'; 3 | import { PasswordService } from '../auth/password.service'; 4 | import { ChangePasswordInput } from './dto/change-password.input'; 5 | import { UpdateUserInput } from './dto/update-user.input'; 6 | 7 | @Injectable() 8 | export class UsersService { 9 | constructor( 10 | private prisma: PrismaService, 11 | private passwordService: PasswordService, 12 | ) {} 13 | 14 | updateUser(userId: string, newUserData: UpdateUserInput) { 15 | return this.prisma.user.update({ 16 | data: newUserData, 17 | where: { 18 | id: userId, 19 | }, 20 | }); 21 | } 22 | 23 | async changePassword( 24 | userId: string, 25 | userPassword: string, 26 | changePassword: ChangePasswordInput, 27 | ) { 28 | const passwordValid = await this.passwordService.validatePassword( 29 | changePassword.oldPassword, 30 | userPassword, 31 | ); 32 | 33 | if (!passwordValid) { 34 | throw new BadRequestException('Invalid password'); 35 | } 36 | 37 | const hashedPassword = await this.passwordService.hashPassword( 38 | changePassword.newPassword, 39 | ); 40 | 41 | return this.prisma.user.update({ 42 | data: { 43 | password: hashedPassword, 44 | }, 45 | where: { id: userId }, 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { Chance } from 'chance'; 5 | import { AppModule } from 'src/app.module'; 6 | 7 | const chance = new Chance(); 8 | 9 | describe('AppController (e2e)', () => { 10 | let app: INestApplication; 11 | 12 | beforeEach(async () => { 13 | const moduleFixture: TestingModule = await Test.createTestingModule({ 14 | imports: [AppModule], 15 | }).compile(); 16 | 17 | app = moduleFixture.createNestApplication(); 18 | await app.init(); 19 | }); 20 | 21 | it('/ (GET)', () => { 22 | return request(app.getHttpServer()) 23 | .get('/') 24 | .expect(200) 25 | .expect('Hello World!'); 26 | }); 27 | 28 | it('/hello/:name (GET)', () => { 29 | const name = chance.name(); 30 | return request(app.getHttpServer()) 31 | .get(`/hello/${name}`) 32 | .expect(200) 33 | .expect(`Hello ${name}!`); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/app.resolver.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { Chance } from 'chance'; 5 | import { AppModule } from 'src/app.module'; 6 | 7 | const chance = new Chance(); 8 | 9 | describe('AppResolver (e2e)', () => { 10 | let app: INestApplication; 11 | 12 | beforeEach(async () => { 13 | const moduleFixture: TestingModule = await Test.createTestingModule({ 14 | imports: [AppModule], 15 | }).compile(); 16 | 17 | app = moduleFixture.createNestApplication(); 18 | await app.init(); 19 | }); 20 | 21 | it('helloWorld (Query)', () => { 22 | // TODO assert return value 23 | return request(app.getHttpServer()) 24 | .post('/graphql') 25 | .send({ 26 | query: '{ helloWorld }', 27 | }) 28 | .expect(200); 29 | }); 30 | it('hello (Query)', () => { 31 | // TODO assert return value 32 | const name = chance.name(); 33 | return request(app.getHttpServer()) 34 | .post('/graphql') 35 | .send({ 36 | query: `{ hello(name: "${name}") }`, 37 | }) 38 | .expect(200); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "prisma"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------