├── .dockerignore ├── .env ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── Dockerfile ├── LICENSE.txt ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── commitlint.config.js ├── docker-compose.yml ├── nest-cli.json ├── package-lock.json ├── package.json ├── prisma ├── migrations │ └── migration_lock.toml ├── schema.prisma ├── seed.ts └── seeds │ ├── heroes.seed.ts │ └── types.seed.ts ├── src ├── app.module.ts ├── common │ ├── decorators │ │ ├── ApiArrayResponse.ts │ │ ├── ApiPaginatedResponse.ts │ │ └── index.ts │ ├── dtos │ │ ├── index.ts │ │ ├── paginate-options.dto.ts │ │ └── paginated.dto.ts │ ├── exception-filters │ │ ├── index.ts │ │ └── prisma-exception-filter.ts │ └── index.ts ├── database │ ├── database.module.ts │ ├── index.ts │ └── prisma.service.ts ├── heroes │ ├── dtos │ │ ├── create-hero.dto.ts │ │ ├── delete-hero.dto.ts │ │ ├── hero.dto.ts │ │ ├── heroes-paginate-options.dto.ts │ │ ├── heroes-paginated.dto.ts │ │ ├── index.ts │ │ └── update-hero.dto.ts │ ├── heroes-repository.interface.ts │ ├── heroes.controller.ts │ ├── heroes.module.ts │ ├── heroes.resolver.ts │ ├── heroes.service.ts │ └── index.ts ├── main.ts └── types │ ├── dtos │ ├── create-type.dto.ts │ ├── delete-type.dto.ts │ ├── index.ts │ ├── type.dto.ts │ ├── types-paginated.dto.ts │ └── update-type.dto.ts │ ├── index.ts │ ├── types.controller.ts │ ├── types.module.ts │ ├── types.resolver.ts │ └── types.service.ts ├── static ├── afro_man.png ├── anime.png ├── apple_watch.png ├── avocado.png ├── baby_kid.png ├── batman.png ├── bear_russian.png ├── breaking_bad.png ├── builder.png ├── cactus.png ├── chaplin.png ├── coffee_zorro.png ├── crying_cloud.png ├── einstein.png ├── female.png ├── geisha.png ├── girl.png ├── girl_kid.png ├── grandma.png ├── halloween.png ├── harley_queen.png ├── hipster.png ├── indian_male.png ├── indian_man_2.png ├── marlin_monroe.png ├── nun_sister.png ├── ozzy.png ├── pencil.png ├── pilot.png ├── punk.png ├── santa_claus.png ├── sheep.png ├── sluggard.png ├── spider.png ├── trump.png ├── ufo.png └── wrestler.png ├── test ├── heroses.e2e-spec.ts ├── jest-e2e.json └── mocks │ └── hero.mock.ts ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | Dockerfile 3 | schema-compiled.graphql 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="mysql://heroes:heroes@mysql/heroes" 2 | DATABASE_USER=heroes 3 | DATABASE_PASSWORD=heroes 4 | DATABASE_NAME=heroes -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run format 5 | npm run lint 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine3.16 AS development 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json /app/package.json 6 | COPY package-lock.json /app/package-lock.json 7 | 8 | RUN npm install 9 | 10 | EXPOSE 3000 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Netguru 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 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Ticket 2 | 3 | https://netguru.atlassian.net/browse/FA-XXX 4 | 5 | ## Description 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] 2 | [![Forks][forks-shield]][forks-url] 3 | [![Stargazers][stars-shield]][stars-url] 4 | [![Issues][issues-shield]][issues-url] 5 | [![MIT License][license-shield]][license-url] 6 | 7 | 8 |
9 |

10 | 11 | Logo 12 | 13 | 14 |

Heroes API

15 | 16 |

17 | A backend application built with Prisma, Docker and NestJS 18 |
19 | Report Bug 20 | · 21 | Request Feature 22 |

23 |

24 | 25 | 26 | 27 | ## Table of Contents 28 | 29 | - [Table of Contents](#table-of-contents) 30 | - [Stack](#stack) 31 | - [Getting Started](#getting-started) 32 | - [Documentation](#documentation) 33 | - [Initial setup](#initial-setup) 34 | - [Running the project](#running-the-project) 35 | - [Authors](#authors) 36 | - [License](#license) 37 | 38 | ## Stack 39 | 40 | This application was built with: 41 | 42 | - [NestJS](https://nestjs.com/) 43 | - [Prisma](https://www.prisma.io/) 44 | - [Node.js](https://node.js.org/) 45 | - [Docker](https://www.docker.com/) 46 | 47 | 48 | 49 | ## Getting Started 50 | 51 | Before you start, make sure you have [Docker](https://docs.docker.com/install/) and [Node](https://nodejs.org/en/) installed on your local machine. 52 | 53 | ### Documentation 54 | 55 | Documentation can be found, after initial setup, for HTTP in [Swagger](http://localhost:3000/swagger/#/), for GraphQL in [GraphQL Playground](http://localhost:3000/graphql). 56 | 57 | ### Initial setup 58 | 59 | 1. Clone this repo into your local machine 60 | 61 | - with **https**
62 | `git clone https://github.com/netguru/heroes-api.git` 63 | - or with **ssh**
64 | `git clone git@github.com:netguru/heroes-api.git` 65 | 66 | 2. Launch Docker compose to run Prisma's and MySQL's images. 67 | `docker compose up -d` 68 | 69 | 3. Open API container's terminal 70 | `docker compose exec api /bin/sh` 71 | 72 | 4. Deploy database schema into the MySQL database. 73 | `npx prisma db push` 74 | 75 | 5. Seed the database with default data. 76 | `npx prisma db seed` 77 | 78 | ### Running the project 79 | 80 | After the initial setup there's no additional work needed, project is running in the background as a Docker container. 81 | 82 | - The REST API is available on your local machine on `http://localhost:3000`. 83 | - Swagger documentation is available on `http://localhost:3000/swagger/`. 84 | - GraphQL playground (and server) is available on `http://localhost:3000/graphql`. 85 | 86 | You can stop it by executing `docker compose stop`, and you can resume it by `docker compose start` 87 | 88 | 89 | 90 | 91 | ## Authors 92 | 93 | 1. Sergiusz Strumiński 94 | 2. Kacper Wojciechowski 95 | 96 | 97 | 98 | ## License 99 | 100 | Distributed under the MIT License. See `LICENSE` for more information. 101 | 102 | 103 | 104 | 105 | [contributors-shield]: https://img.shields.io/github/contributors/othneildrew/Best-README-Template.svg?style=flat-square 106 | [contributors-url]: https://github.com/netguru/heroes-api/graphs/contributors 107 | [forks-shield]: https://img.shields.io/github/forks/netguru/heroes-api 108 | [forks-url]: https://github.com/netguru/heroes-api/network/members 109 | [stars-shield]: https://img.shields.io/github/stars/netguru/heroes-api 110 | [stars-url]: https://github.com/netguru/heroes-api/stargazers 111 | [issues-shield]: https://img.shields.io/github/issues/netguru/heroes-api 112 | [issues-url]: https://github.com/netguru/heroes-api/issues 113 | [license-shield]: https://img.shields.io/github/license/netguru/heroes-api 114 | [license-url]: https://github.com/netguru/heroes-api/blob/master/LICENSE.txt 115 | [product-screenshot]: images/screenshot.png 116 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | } 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | mysql: 4 | platform: linux/x86_64 5 | image: mysql:8 6 | restart: always 7 | environment: 8 | MYSQL_RANDOM_ROOT_PASSWORD: 'yes' 9 | MYSQL_USER: $DATABASE_USER 10 | MYSQL_PASSWORD: $DATABASE_PASSWORD 11 | MYSQL_DATABASE: $DATABASE_NAME 12 | volumes: 13 | - mysql:/var/lib/mysql 14 | 15 | api: 16 | platform: linux/x86_64 17 | build: 18 | dockerfile: Dockerfile 19 | context: . 20 | target: development 21 | command: npm run start:dev 22 | environment: 23 | DATABASE_URL: $DATABASE_URL 24 | volumes: 25 | - .:/app 26 | - /app/node_modules/ 27 | depends_on: 28 | - mysql 29 | ports: 30 | - "3000:3000" 31 | 32 | adminer: 33 | image: adminer 34 | restart: always 35 | ports: 36 | - "8080:8080" 37 | 38 | volumes: 39 | mysql: ~ 40 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heroes-api", 3 | "version": "2.0.0", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "postinstall": "husky install", 10 | "prebuild": "rimraf dist", 11 | "build": "nest build", 12 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 13 | "start": "nest start", 14 | "start:dev": "nest start --watch", 15 | "start:debug": "nest start --debug --watch", 16 | "start:prod": "node dist/main", 17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^8.0.0", 26 | "@nestjs/config": "^1.1.0", 27 | "@nestjs/core": "^8.0.0", 28 | "@nestjs/graphql": "^9.1.1", 29 | "@nestjs/platform-express": "^8.0.0", 30 | "@nestjs/swagger": "^5.1.4", 31 | "@prisma/client": "^3.6.0", 32 | "apollo-server-express": "^3.5.0", 33 | "class-transformer": "^0.4.0", 34 | "class-validator": "^0.13.1", 35 | "graphql": "^15.7.2", 36 | "reflect-metadata": "^0.1.13", 37 | "rimraf": "^3.0.2", 38 | "rxjs": "^7.2.0", 39 | "swagger-ui-express": "^4.1.6" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^14.1.0", 43 | "@commitlint/config-conventional": "^14.1.0", 44 | "@nestjs/cli": "^8.0.0", 45 | "@nestjs/schematics": "^8.0.0", 46 | "@nestjs/testing": "^8.2.3", 47 | "@types/express": "^4.17.13", 48 | "@types/jest": "^27.0.1", 49 | "@types/node": "^16.0.0", 50 | "@types/supertest": "^2.0.11", 51 | "@typescript-eslint/eslint-plugin": "^5.0.0", 52 | "@typescript-eslint/parser": "^5.0.0", 53 | "eslint": "^8.0.1", 54 | "eslint-config-prettier": "^8.3.0", 55 | "eslint-plugin-prettier": "^4.0.0", 56 | "husky": "^7.0.4", 57 | "jest": "^27.2.5", 58 | "prettier": "^2.3.2", 59 | "prettier-plugin-prisma": "^3.5.0", 60 | "prisma": "^3.6.0", 61 | "source-map-support": "^0.5.20", 62 | "supertest": "^6.1.3", 63 | "ts-jest": "^27.0.3", 64 | "ts-loader": "^9.2.3", 65 | "ts-node": "^10.0.0", 66 | "tsconfig-paths": "^3.10.1", 67 | "typescript": "^4.3.5" 68 | }, 69 | "prisma": { 70 | "seed": "ts-node prisma/seed.ts" 71 | }, 72 | "jest": { 73 | "moduleFileExtensions": [ 74 | "js", 75 | "json", 76 | "ts" 77 | ], 78 | "rootDir": "src", 79 | "testRegex": ".*\\.spec\\.ts$", 80 | "transform": { 81 | "^.+\\.(t|j)s$": "ts-jest" 82 | }, 83 | "collectCoverageFrom": [ 84 | "**/*.(t|j)s" 85 | ], 86 | "coverageDirectory": "../coverage", 87 | "testEnvironment": "node" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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 = "mysql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Hero { 11 | id String @id @default(uuid()) 12 | fullName String 13 | avatarUrl String 14 | description String @db.Text 15 | type Type @relation(references: [id], fields: [typeId]) 16 | typeId String 17 | } 18 | 19 | model Type { 20 | id String @id @default(uuid()) 21 | name String 22 | hero Hero[] 23 | } 24 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import { 3 | animalTypeSeed, 4 | humanTypeSeed, 5 | plantTypeSeed, 6 | otherTypeSeed, 7 | } from './seeds/types.seed'; 8 | import { 9 | humanHeroesSeed, 10 | animalHeroesSeed, 11 | plantHeroesSeed, 12 | otherHeroesSeed, 13 | } from './seeds/heroes.seed'; 14 | 15 | const prisma = new PrismaClient(); 16 | 17 | const sentence = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ullamcorper aliquet porttitor.`; 18 | 19 | async function main() { 20 | const [humanType, animalType, plantType, otherType] = await Promise.all([ 21 | prisma.type.create({ data: humanTypeSeed }), 22 | prisma.type.create({ data: animalTypeSeed }), 23 | prisma.type.create({ data: plantTypeSeed }), 24 | prisma.type.create({ data: otherTypeSeed }), 25 | ]); 26 | 27 | await prisma.hero.createMany({ 28 | data: animalHeroesSeed.map((heroSeed) => ({ 29 | description: 'Lorem ipsum', 30 | typeId: animalType.id, 31 | ...heroSeed, 32 | })), 33 | }); 34 | 35 | await prisma.hero.createMany({ 36 | data: humanHeroesSeed.map((heroSeed) => ({ 37 | description: sentence, 38 | typeId: humanType.id, 39 | ...heroSeed, 40 | })), 41 | }); 42 | 43 | await prisma.hero.createMany({ 44 | data: plantHeroesSeed.map((heroSeed) => ({ 45 | description: sentence, 46 | typeId: plantType.id, 47 | ...heroSeed, 48 | })), 49 | }); 50 | 51 | await prisma.hero.createMany({ 52 | data: otherHeroesSeed.map((heroSeed) => ({ 53 | description: sentence, 54 | typeId: otherType.id, 55 | ...heroSeed, 56 | })), 57 | }); 58 | } 59 | 60 | main() 61 | .catch((e) => { 62 | console.error(e); 63 | process.exit(1); 64 | }) 65 | .finally(async () => { 66 | await prisma.$disconnect(); 67 | }); 68 | -------------------------------------------------------------------------------- /prisma/seeds/heroes.seed.ts: -------------------------------------------------------------------------------- 1 | const ASSETS_PATH = '/static'; 2 | 3 | export const humanHeroesSeed = [ 4 | { 5 | fullName: 'Batman', 6 | avatarUrl: `${ASSETS_PATH}/batman.png`, 7 | }, 8 | { 9 | fullName: 'Wrestler', 10 | avatarUrl: `${ASSETS_PATH}/wrestler.png`, 11 | }, 12 | { 13 | fullName: 'Donald Trump', 14 | avatarUrl: `${ASSETS_PATH}/trump.png`, 15 | }, 16 | { 17 | fullName: 'Harley Quinn', 18 | avatarUrl: `${ASSETS_PATH}/harley_queen.png`, 19 | }, 20 | 21 | { 22 | fullName: 'Albert Einstain', 23 | avatarUrl: `${ASSETS_PATH}/einstein.png`, 24 | }, 25 | { 26 | fullName: 'Ozzy', 27 | avatarUrl: `${ASSETS_PATH}/ozzy.png`, 28 | }, 29 | ]; 30 | export const plantHeroesSeed = [ 31 | { 32 | fullName: 'The Cactus', 33 | avatarUrl: `${ASSETS_PATH}/cactus.png`, 34 | }, 35 | { 36 | fullName: 'The Avocado', 37 | avatarUrl: `${ASSETS_PATH}/avocado.png`, 38 | }, 39 | ]; 40 | export const animalHeroesSeed = [ 41 | { 42 | fullName: 'Sluggard', 43 | avatarUrl: `${ASSETS_PATH}/sluggard.png`, 44 | }, 45 | { 46 | fullName: 'Cool Sheep', 47 | avatarUrl: `${ASSETS_PATH}/sheep.png`, 48 | }, 49 | ]; 50 | export const otherHeroesSeed = [ 51 | { 52 | fullName: 'Shelba', 53 | avatarUrl: `${ASSETS_PATH}/spider.png`, 54 | }, 55 | { 56 | fullName: 'UFO', 57 | avatarUrl: `${ASSETS_PATH}/ufo.png`, 58 | }, 59 | ]; 60 | -------------------------------------------------------------------------------- /prisma/seeds/types.seed.ts: -------------------------------------------------------------------------------- 1 | export const humanTypeSeed = { 2 | name: 'Human', 3 | }; 4 | 5 | export const animalTypeSeed = { 6 | name: 'Animal', 7 | }; 8 | 9 | export const otherTypeSeed = { 10 | name: 'Other', 11 | }; 12 | 13 | export const plantTypeSeed = { 14 | name: 'Plant', 15 | }; 16 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GraphQLModule } from '@nestjs/graphql'; 3 | import { HeroesModule } from './heroes'; 4 | import { DatabaseModule } from './database'; 5 | import { TypesModule } from './types'; 6 | 7 | @Module({ 8 | imports: [ 9 | GraphQLModule.forRoot({ 10 | autoSchemaFile: 'schema.gql', 11 | cors: true, 12 | }), 13 | DatabaseModule, 14 | HeroesModule, 15 | TypesModule, 16 | ], 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /src/common/decorators/ApiArrayResponse.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, Type } from '@nestjs/common'; 2 | import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger'; 3 | 4 | export const ApiArrayResponse = (model: TModel) => 5 | applyDecorators( 6 | ApiOkResponse({ 7 | schema: { 8 | type: 'array', 9 | items: { $ref: getSchemaPath(model) }, 10 | }, 11 | }), 12 | ); 13 | -------------------------------------------------------------------------------- /src/common/decorators/ApiPaginatedResponse.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, Type } from '@nestjs/common'; 2 | import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger'; 3 | 4 | export const ApiPaginatedResponse = (model: TModel) => 5 | applyDecorators( 6 | ApiOkResponse({ 7 | schema: { 8 | allOf: [ 9 | { 10 | properties: { 11 | data: { 12 | type: 'array', 13 | items: { $ref: getSchemaPath(model) }, 14 | }, 15 | totalCount: { 16 | type: 'number', 17 | }, 18 | }, 19 | }, 20 | ], 21 | }, 22 | }), 23 | ); 24 | -------------------------------------------------------------------------------- /src/common/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ApiPaginatedResponse'; 2 | export * from './ApiArrayResponse'; 3 | -------------------------------------------------------------------------------- /src/common/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './paginated.dto'; 2 | export * from './paginate-options.dto'; 3 | -------------------------------------------------------------------------------- /src/common/dtos/paginate-options.dto.ts: -------------------------------------------------------------------------------- 1 | import { ArgsType, Field, Int } from '@nestjs/graphql'; 2 | import { IsNumber, IsOptional } from 'class-validator'; 3 | import { ApiPropertyOptional } from '@nestjs/swagger'; 4 | import { Type } from 'class-transformer'; 5 | 6 | @ArgsType() 7 | export class PaginateOptionsDto { 8 | @IsNumber() 9 | @IsOptional() 10 | @ApiPropertyOptional() 11 | @Type(() => Number) 12 | @Field(() => Int, { nullable: true }) 13 | skip: number; 14 | 15 | @IsNumber() 16 | @IsOptional() 17 | @ApiPropertyOptional() 18 | @Type(() => Number) 19 | @Field(() => Int, { nullable: true }) 20 | first: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/common/dtos/paginated.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsArray, IsNumber } from 'class-validator'; 2 | import { Field, Int, ObjectType } from '@nestjs/graphql'; 3 | 4 | export interface ClassType { 5 | new (...args: any[]): T; 6 | } 7 | 8 | export function PaginatedDto(TItemClass: ClassType) { 9 | @ObjectType({ isAbstract: true }) 10 | abstract class PaginatedResponseClass { 11 | @IsArray() 12 | @Field(() => [TItemClass]) 13 | data: TData[]; 14 | 15 | @IsNumber() 16 | @Field(() => Int) 17 | totalCount: number; 18 | } 19 | return PaginatedResponseClass; 20 | } 21 | -------------------------------------------------------------------------------- /src/common/exception-filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prisma-exception-filter'; 2 | -------------------------------------------------------------------------------- /src/common/exception-filters/prisma-exception-filter.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { Prisma } from '@prisma/client'; 4 | 5 | const hasCauseProperty = (obj: T): obj is T & { cause: string } => { 6 | return Object.prototype.hasOwnProperty.call(obj, 'cause'); 7 | }; 8 | 9 | @Catch(Prisma.PrismaClientKnownRequestError) 10 | export class PrismaExceptionFilter implements ExceptionFilter { 11 | catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) { 12 | const ctx = host.switchToHttp(); 13 | const response = ctx.getResponse(); 14 | const status = exception.code; 15 | const meta = exception.meta; 16 | 17 | /** 18 | * @see https://www.prisma.io/docs/reference/api-reference/error-reference#p2025 19 | */ 20 | if (status === 'P2025') { 21 | const defaultError = 22 | 'An operation failed because it depends on one or more records that were required but not found.'; 23 | const error = hasCauseProperty(meta) ? meta.cause : defaultError; 24 | return response.status(400).json({ 25 | statusCode: 400, 26 | error, 27 | }); 28 | } 29 | return response.status(500).json({ 30 | statusCode: status, 31 | error: exception.message, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorators'; 2 | export * from './dtos'; 3 | export * from './exception-filters'; 4 | -------------------------------------------------------------------------------- /src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | @Module({ 5 | exports: [PrismaService], 6 | providers: [PrismaService], 7 | }) 8 | export class DatabaseModule {} 9 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './database.module'; 2 | export * from './prisma.service'; 3 | -------------------------------------------------------------------------------- /src/database/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import { 3 | INestApplication, 4 | Injectable, 5 | NotFoundException, 6 | OnModuleInit, 7 | } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class PrismaService extends PrismaClient implements OnModuleInit { 11 | constructor() { 12 | super({ 13 | rejectOnNotFound: { 14 | findUnique: (err) => new NotFoundException(err.message), 15 | findFirst: (err) => new NotFoundException(err.message), 16 | }, 17 | }); 18 | } 19 | 20 | async onModuleInit() { 21 | await this.$connect(); 22 | } 23 | 24 | async enableShutdownHooks(app: INestApplication) { 25 | this.$on('beforeExit', async () => { 26 | await app.close(); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/heroes/dtos/create-hero.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsUrl } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Field, ID, InputType } from '@nestjs/graphql'; 4 | 5 | @InputType('CreateHeroInput') 6 | export class CreateHeroDto { 7 | @IsString() 8 | @ApiProperty() 9 | @Field() 10 | fullName: string; 11 | 12 | @IsString() 13 | @ApiProperty() 14 | @Field(() => ID) 15 | typeId: string; 16 | 17 | @IsString() 18 | @IsUrl() 19 | @ApiProperty() 20 | @Field() 21 | avatarUrl: string; 22 | 23 | @IsString() 24 | @ApiProperty() 25 | @Field() 26 | description: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/heroes/dtos/delete-hero.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Field, ID, InputType } from '@nestjs/graphql'; 3 | import { IsString } from 'class-validator'; 4 | 5 | @InputType('DeleteHeroInput') 6 | export class DeleteHeroDto { 7 | @IsString() 8 | @ApiProperty() 9 | @Field(() => ID) 10 | readonly id: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/heroes/dtos/hero.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Field, ID, ObjectType } from '@nestjs/graphql'; 3 | import { TypeDto } from '../../types/dtos'; 4 | 5 | @ObjectType('Hero') 6 | export class HeroDto { 7 | @ApiProperty() 8 | @Field(() => ID) 9 | readonly id: string; 10 | 11 | @ApiProperty() 12 | @Field() 13 | fullName: string; 14 | 15 | @ApiProperty() 16 | @Field() 17 | avatarUrl: string; 18 | 19 | @ApiProperty() 20 | @Field() 21 | description: string; 22 | 23 | @ApiProperty({ type: TypeDto }) 24 | @Field(() => TypeDto) 25 | type: TypeDto; 26 | } 27 | -------------------------------------------------------------------------------- /src/heroes/dtos/heroes-paginate-options.dto.ts: -------------------------------------------------------------------------------- 1 | import { ArgsType, Field, ID, Int } from '@nestjs/graphql'; 2 | import { IsNumber, IsOptional, IsString } from 'class-validator'; 3 | import { ApiPropertyOptional } from '@nestjs/swagger'; 4 | import { Type } from 'class-transformer'; 5 | 6 | @ArgsType() 7 | export class HeroesPaginateOptionsDto { 8 | @IsString() 9 | @IsOptional() 10 | @ApiPropertyOptional() 11 | @Type(() => String) 12 | @Field(() => ID, { nullable: true }) 13 | typeId: string; 14 | 15 | @IsString() 16 | @IsOptional() 17 | @ApiPropertyOptional() 18 | @Type(() => String) 19 | @Field({ nullable: true }) 20 | fullNameQuery: string; 21 | 22 | @IsNumber() 23 | @IsOptional() 24 | @ApiPropertyOptional() 25 | @Type(() => Number) 26 | @Field(() => Int, { nullable: true }) 27 | skip: number; 28 | 29 | @IsNumber() 30 | @IsOptional() 31 | @ApiPropertyOptional() 32 | @Type(() => Number) 33 | @Field(() => Int, { nullable: true }) 34 | first: number; 35 | } 36 | -------------------------------------------------------------------------------- /src/heroes/dtos/heroes-paginated.dto.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType } from '@nestjs/graphql'; 2 | import { PaginatedDto } from '../../common'; 3 | import { HeroDto } from './hero.dto'; 4 | 5 | @ObjectType('HeroesPaginated') 6 | export class HeroesPaginatedDto extends PaginatedDto(HeroDto) {} 7 | -------------------------------------------------------------------------------- /src/heroes/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-hero.dto'; 2 | export * from './update-hero.dto'; 3 | export * from './delete-hero.dto'; 4 | export * from './hero.dto'; 5 | export * from './heroes-paginated.dto'; 6 | export * from './heroes-paginate-options.dto'; 7 | -------------------------------------------------------------------------------- /src/heroes/dtos/update-hero.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Field, ID, InputType } from '@nestjs/graphql'; 3 | import { IsString, IsUrl } from 'class-validator'; 4 | 5 | @InputType('UpdateHeroInput') 6 | export class UpdateHeroDto { 7 | @IsString() 8 | @ApiProperty() 9 | @Field(() => ID) 10 | readonly id: string; 11 | 12 | @IsString() 13 | @ApiProperty() 14 | @Field() 15 | fullName?: string; 16 | 17 | @IsString() 18 | @IsUrl() 19 | @ApiProperty() 20 | @Field() 21 | avatarUrl?: string; 22 | 23 | @IsString() 24 | @ApiProperty() 25 | @Field(() => ID) 26 | readonly typeId?: string; 27 | 28 | @IsString() 29 | @ApiProperty() 30 | @Field() 31 | description?: string; 32 | } 33 | -------------------------------------------------------------------------------- /src/heroes/heroes-repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from '@prisma/client'; 2 | import { HeroDto } from './dtos'; 3 | 4 | export interface HeroesRepository { 5 | heroes(args?: Omit): Promise; 6 | 7 | hero(args?: Omit): Promise; 8 | 9 | count(args?: Prisma.HeroCountArgs): Promise; 10 | 11 | create(data: Prisma.HeroCreateInput): Promise; 12 | 13 | update( 14 | where: Prisma.HeroWhereUniqueInput, 15 | data: Prisma.HeroUpdateInput, 16 | ): Promise; 17 | 18 | delete(where: Prisma.HeroWhereUniqueInput): Promise; 19 | 20 | random(): Promise; 21 | } 22 | -------------------------------------------------------------------------------- /src/heroes/heroes.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | Query, 10 | } from '@nestjs/common'; 11 | import { 12 | ApiBadRequestResponse, 13 | ApiOkResponse, 14 | ApiNotFoundResponse, 15 | ApiTags, 16 | } from '@nestjs/swagger'; 17 | import { ApiPaginatedResponse } from '../common'; 18 | import { HeroesService } from './heroes.service'; 19 | import { 20 | CreateHeroDto, 21 | UpdateHeroDto, 22 | HeroDto, 23 | HeroesPaginatedDto, 24 | HeroesPaginateOptionsDto, 25 | } from './dtos'; 26 | 27 | @Controller('heroes') 28 | @ApiTags('heroes') 29 | export class HeroesController { 30 | constructor(private readonly heroesService: HeroesService) {} 31 | 32 | @Get() 33 | @ApiPaginatedResponse(HeroDto) 34 | async getAll( 35 | @Query() query: HeroesPaginateOptionsDto, 36 | ): Promise { 37 | const whereOptions = { 38 | typeId: query.typeId, 39 | fullName: { 40 | contains: query.fullNameQuery, 41 | }, 42 | }; 43 | 44 | const [data, totalCount] = await Promise.all([ 45 | this.heroesService.heroes({ 46 | take: query.first, 47 | skip: query.skip, 48 | where: whereOptions, 49 | }), 50 | this.heroesService.count({ 51 | where: whereOptions, 52 | }), 53 | ]); 54 | 55 | return { data, totalCount }; 56 | } 57 | 58 | @Get('/random') 59 | @ApiOkResponse({ type: HeroDto }) 60 | getRandom(): Promise { 61 | return this.heroesService.random(); 62 | } 63 | 64 | @Get(':id') 65 | @ApiNotFoundResponse() 66 | @ApiOkResponse({ type: HeroDto }) 67 | getOne(@Param('id') id: string): Promise { 68 | return this.heroesService.hero({ where: { id } }); 69 | } 70 | 71 | @Post() 72 | @ApiOkResponse({ type: HeroDto }) 73 | @ApiBadRequestResponse() 74 | create(@Body() createHeroDto: CreateHeroDto): Promise { 75 | const { typeId, ...hero } = createHeroDto; 76 | return this.heroesService.create({ 77 | ...hero, 78 | type: { connect: { id: typeId } }, 79 | }); 80 | } 81 | 82 | @Put(':id') 83 | @ApiOkResponse({ type: HeroDto }) 84 | @ApiBadRequestResponse() 85 | @ApiNotFoundResponse() 86 | update( 87 | @Param('id') id: string, 88 | @Body() updateHeroDto: UpdateHeroDto, 89 | ): Promise { 90 | const { typeId, ...hero } = updateHeroDto; 91 | return this.heroesService.update( 92 | { id }, 93 | { 94 | ...hero, 95 | type: { connect: { id: typeId } }, 96 | }, 97 | ); 98 | } 99 | 100 | @Delete(':id') 101 | @ApiOkResponse({ type: HeroDto }) 102 | @ApiNotFoundResponse() 103 | delete(@Param('id') id: string) { 104 | return this.heroesService.delete({ id }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/heroes/heroes.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypesModule } from '../types'; 3 | import { DatabaseModule } from '../database'; 4 | import { HeroesService } from './heroes.service'; 5 | import { HeroesController } from './heroes.controller'; 6 | import { HeroesResolver } from './heroes.resolver'; 7 | 8 | @Module({ 9 | imports: [DatabaseModule, TypesModule], 10 | providers: [HeroesService, HeroesResolver], 11 | controllers: [HeroesController], 12 | }) 13 | export class HeroesModule {} 14 | -------------------------------------------------------------------------------- /src/heroes/heroes.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, ID, Mutation, Query, Resolver } from '@nestjs/graphql'; 2 | import { 3 | CreateHeroDto, 4 | HeroDto, 5 | HeroesPaginatedDto, 6 | HeroesPaginateOptionsDto, 7 | UpdateHeroDto, 8 | DeleteHeroDto, 9 | } from './dtos'; 10 | import { HeroesService } from './heroes.service'; 11 | 12 | @Resolver(() => HeroDto) 13 | export class HeroesResolver { 14 | constructor(private readonly heroesService: HeroesService) {} 15 | 16 | @Query(() => HeroesPaginatedDto) 17 | async heroes( 18 | @Args() heroesPaginateOptions: HeroesPaginateOptionsDto, 19 | ): Promise { 20 | const whereOptions = { 21 | typeId: heroesPaginateOptions.typeId, 22 | fullName: { 23 | contains: heroesPaginateOptions.fullNameQuery, 24 | }, 25 | }; 26 | 27 | const [data, totalCount] = await Promise.all([ 28 | this.heroesService.heroes({ 29 | take: heroesPaginateOptions.first, 30 | skip: heroesPaginateOptions.skip, 31 | where: whereOptions, 32 | }), 33 | this.heroesService.count({ 34 | where: whereOptions, 35 | }), 36 | ]); 37 | 38 | return { data, totalCount }; 39 | } 40 | 41 | @Query(() => HeroDto) 42 | hero(@Args('id', { type: () => ID }) id: string): Promise { 43 | return this.heroesService.hero({ where: { id } }); 44 | } 45 | 46 | @Query(() => HeroDto) 47 | randomHero(): Promise { 48 | return this.heroesService.random(); 49 | } 50 | 51 | @Mutation(() => HeroDto) 52 | createHero(@Args('input') input: CreateHeroDto): Promise { 53 | const { typeId, ...hero } = input; 54 | return this.heroesService.create({ 55 | ...hero, 56 | type: { connect: { id: typeId } }, 57 | }); 58 | } 59 | 60 | @Mutation(() => HeroDto) 61 | updateHero(@Args('input') input: UpdateHeroDto): Promise { 62 | const { id, typeId, ...hero } = input; 63 | return this.heroesService.update( 64 | { id }, 65 | { 66 | ...hero, 67 | type: { connect: { id: typeId } }, 68 | }, 69 | ); 70 | } 71 | 72 | @Mutation(() => HeroDto) 73 | deleteHero(@Args('input') input: DeleteHeroDto): Promise { 74 | return this.heroesService.delete({ id: input.id }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/heroes/heroes.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Prisma } from '@prisma/client'; 3 | import { PrismaService } from '../database'; 4 | import { HeroesRepository } from './heroes-repository.interface'; 5 | 6 | @Injectable() 7 | export class HeroesService implements HeroesRepository { 8 | constructor(private readonly database: PrismaService) {} 9 | 10 | heroes(args?: Omit) { 11 | return this.database.hero.findMany({ 12 | ...args, 13 | include: { type: true }, 14 | }); 15 | } 16 | 17 | hero(args?: Omit) { 18 | return this.database.hero.findUnique({ 19 | ...args, 20 | include: { type: true }, 21 | }); 22 | } 23 | 24 | count(args?: Prisma.HeroCountArgs) { 25 | return this.database.hero.count(args); 26 | } 27 | 28 | async create(data: Prisma.HeroCreateInput) { 29 | return this.database.hero.create({ 30 | data, 31 | include: { type: true }, 32 | }); 33 | } 34 | 35 | update(where: Prisma.HeroWhereUniqueInput, data: Prisma.HeroUpdateInput) { 36 | return this.database.hero.update({ 37 | where, 38 | data, 39 | include: { type: true }, 40 | }); 41 | } 42 | 43 | delete(where: Prisma.HeroWhereUniqueInput) { 44 | return this.database.hero.delete({ 45 | where, 46 | include: { type: true }, 47 | }); 48 | } 49 | 50 | async random() { 51 | const count = await this.count(); 52 | const skip = Math.floor(Math.random() * count); 53 | const [hero] = await this.heroes({ skip, take: 1 }); 54 | return hero; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/heroes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './heroes.service'; 2 | export * from './heroes.module'; 3 | export * from './heroes-repository.interface'; 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 5 | import { NestExpressApplication } from '@nestjs/platform-express'; 6 | import { PrismaExceptionFilter } from './common'; 7 | import { AppModule } from './app.module'; 8 | 9 | async function bootstrap() { 10 | const app = await NestFactory.create(AppModule, { 11 | cors: true, 12 | }); 13 | 14 | const config = new DocumentBuilder() 15 | .setTitle('Netguru Heroes') 16 | .setVersion('1.0') 17 | .build(); 18 | const document = SwaggerModule.createDocument(app, config); 19 | SwaggerModule.setup('/swagger', app, document); 20 | 21 | app.useGlobalPipes(new ValidationPipe({ transform: true })); 22 | app.useGlobalFilters(new PrismaExceptionFilter()); 23 | app.useStaticAssets(join(__dirname, '..', '..', 'static'), { 24 | prefix: '/static/', 25 | }); 26 | 27 | await app.listen(3000); 28 | } 29 | bootstrap(); 30 | -------------------------------------------------------------------------------- /src/types/dtos/create-type.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Field, InputType } from '@nestjs/graphql'; 4 | 5 | @InputType('CreateTypeInput') 6 | export class CreateTypeDto { 7 | @IsString() 8 | @ApiProperty() 9 | @Field() 10 | name: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/dtos/delete-type.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Field, ID, InputType } from '@nestjs/graphql'; 3 | import { IsString } from 'class-validator'; 4 | 5 | @InputType('DeleteTypeInput') 6 | export class DeleteTypeDto { 7 | @IsString() 8 | @ApiProperty() 9 | @Field(() => ID) 10 | readonly id: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-type.dto'; 2 | export * from './update-type.dto'; 3 | export * from './delete-type.dto'; 4 | export * from './type.dto'; 5 | export * from './types-paginated.dto'; 6 | -------------------------------------------------------------------------------- /src/types/dtos/type.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Field, ID, ObjectType } from '@nestjs/graphql'; 3 | 4 | @ObjectType('Type') 5 | export class TypeDto { 6 | @ApiProperty() 7 | @Field(() => ID) 8 | readonly id: string; 9 | 10 | @ApiProperty() 11 | @Field(() => ID) 12 | name: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/types/dtos/types-paginated.dto.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType } from '@nestjs/graphql'; 2 | import { PaginatedDto } from '../../common'; 3 | import { TypeDto } from './type.dto'; 4 | 5 | @ObjectType('TypesPaginated') 6 | export class TypesPaginatedDto extends PaginatedDto(TypeDto) {} 7 | -------------------------------------------------------------------------------- /src/types/dtos/update-type.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { Field, ID, InputType } from '@nestjs/graphql'; 3 | import { IsString } from 'class-validator'; 4 | 5 | @InputType('UpdateTypeInput') 6 | export class UpdateTypeDto { 7 | @IsString() 8 | @ApiProperty() 9 | @Field(() => ID) 10 | readonly id: string; 11 | 12 | @IsString() 13 | @ApiPropertyOptional() 14 | @Field({ nullable: true }) 15 | name?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types.service'; 2 | export * from './types.module'; 3 | -------------------------------------------------------------------------------- /src/types/types.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | } from '@nestjs/common'; 10 | import { 11 | ApiBadRequestResponse, 12 | ApiCreatedResponse, 13 | ApiNotFoundResponse, 14 | ApiOkResponse, 15 | ApiTags, 16 | } from '@nestjs/swagger'; 17 | import { TypesService } from './types.service'; 18 | import { CreateTypeDto, UpdateTypeDto, TypeDto } from './dtos'; 19 | import { ApiArrayResponse } from '../common'; 20 | 21 | @Controller('types') 22 | @ApiTags('types') 23 | export class TypesController { 24 | constructor(private readonly typesService: TypesService) {} 25 | 26 | @Get() 27 | @ApiArrayResponse(TypeDto) 28 | getAll(): Promise { 29 | return this.typesService.types(); 30 | } 31 | 32 | @Post() 33 | @ApiCreatedResponse({ type: TypeDto }) 34 | @ApiBadRequestResponse() 35 | @ApiNotFoundResponse() 36 | create(@Body() createTagDto: CreateTypeDto): Promise { 37 | return this.typesService.create(createTagDto); 38 | } 39 | 40 | @Get(':id') 41 | @ApiOkResponse({ type: TypeDto }) 42 | @ApiBadRequestResponse() 43 | @ApiNotFoundResponse() 44 | get(@Param('id') id: string) { 45 | return this.typesService.type({ where: { id } }); 46 | } 47 | 48 | @Put(':id') 49 | @ApiOkResponse({ type: TypeDto }) 50 | @ApiBadRequestResponse() 51 | @ApiNotFoundResponse() 52 | update( 53 | @Param('id') id: string, 54 | @Body() updateTagDto: UpdateTypeDto, 55 | ): Promise { 56 | return this.typesService.update({ id }, updateTagDto); 57 | } 58 | 59 | @Delete(':id') 60 | @ApiOkResponse({ type: TypeDto }) 61 | @ApiNotFoundResponse() 62 | async delete(@Param('id') id: string) { 63 | await this.typesService.delete({ id }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/types/types.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DatabaseModule } from '../database'; 3 | import { TypesService } from './types.service'; 4 | import { TypesController } from './types.controller'; 5 | import { TypesResolver } from './types.resolver'; 6 | 7 | @Module({ 8 | imports: [DatabaseModule], 9 | providers: [TypesService, TypesResolver], 10 | controllers: [TypesController], 11 | }) 12 | export class TypesModule {} 13 | -------------------------------------------------------------------------------- /src/types/types.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, ID, Mutation, Query, Resolver } from '@nestjs/graphql'; 2 | import { PaginateOptionsDto } from '../common'; 3 | import { 4 | CreateTypeDto, 5 | DeleteTypeDto, 6 | TypeDto, 7 | TypesPaginatedDto, 8 | UpdateTypeDto, 9 | } from './dtos'; 10 | import { TypesService } from './types.service'; 11 | 12 | @Resolver(() => TypeDto) 13 | export class TypesResolver { 14 | constructor(private readonly typesService: TypesService) {} 15 | 16 | @Query(() => TypesPaginatedDto) 17 | async types(@Args() options: PaginateOptionsDto): Promise { 18 | const [data, totalCount] = await Promise.all([ 19 | this.typesService.types({ take: options.first, skip: options.skip }), 20 | this.typesService.count(), 21 | ]); 22 | return { data, totalCount }; 23 | } 24 | 25 | @Query(() => TypeDto) 26 | type(@Args('id', { type: () => ID }) id: string): Promise { 27 | return this.typesService.types({ where: { id } }); 28 | } 29 | 30 | @Mutation(() => TypeDto) 31 | createType(@Args('input') input: CreateTypeDto): Promise { 32 | return this.typesService.create(input); 33 | } 34 | 35 | @Mutation(() => TypeDto) 36 | updateType(@Args('input') input: UpdateTypeDto): Promise { 37 | const { id, ...type } = input; 38 | return this.typesService.update({ id }, type); 39 | } 40 | 41 | @Mutation(() => TypeDto) 42 | deleteType(@Args('input') input: DeleteTypeDto): Promise { 43 | return this.typesService.delete({ id: input.id }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/types/types.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Prisma } from '@prisma/client'; 3 | import { PrismaService } from '../database'; 4 | import { TypeDto } from './dtos'; 5 | 6 | @Injectable() 7 | export class TypesService { 8 | constructor(private readonly database: PrismaService) {} 9 | 10 | types(args?: Prisma.TypeFindManyArgs) { 11 | return this.database.type.findMany(args); 12 | } 13 | 14 | async type(args?: Omit) { 15 | return this.database.type.findUnique({ 16 | rejectOnNotFound: true, 17 | ...args, 18 | }); 19 | } 20 | 21 | count(): Promise { 22 | return this.database.type.count(); 23 | } 24 | 25 | create(data: Prisma.TypeCreateInput): Promise { 26 | return this.database.type.create({ data }); 27 | } 28 | 29 | update(where: Prisma.TypeWhereUniqueInput, data: Prisma.TypeUpdateInput) { 30 | return this.database.type.update({ 31 | where, 32 | data, 33 | }); 34 | } 35 | 36 | delete(where: Prisma.TypeWhereUniqueInput) { 37 | return this.database.type.delete({ where }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /static/afro_man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/afro_man.png -------------------------------------------------------------------------------- /static/anime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/anime.png -------------------------------------------------------------------------------- /static/apple_watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/apple_watch.png -------------------------------------------------------------------------------- /static/avocado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/avocado.png -------------------------------------------------------------------------------- /static/baby_kid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/baby_kid.png -------------------------------------------------------------------------------- /static/batman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/batman.png -------------------------------------------------------------------------------- /static/bear_russian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/bear_russian.png -------------------------------------------------------------------------------- /static/breaking_bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/breaking_bad.png -------------------------------------------------------------------------------- /static/builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/builder.png -------------------------------------------------------------------------------- /static/cactus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/cactus.png -------------------------------------------------------------------------------- /static/chaplin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/chaplin.png -------------------------------------------------------------------------------- /static/coffee_zorro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/coffee_zorro.png -------------------------------------------------------------------------------- /static/crying_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/crying_cloud.png -------------------------------------------------------------------------------- /static/einstein.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/einstein.png -------------------------------------------------------------------------------- /static/female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/female.png -------------------------------------------------------------------------------- /static/geisha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/geisha.png -------------------------------------------------------------------------------- /static/girl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/girl.png -------------------------------------------------------------------------------- /static/girl_kid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/girl_kid.png -------------------------------------------------------------------------------- /static/grandma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/grandma.png -------------------------------------------------------------------------------- /static/halloween.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/halloween.png -------------------------------------------------------------------------------- /static/harley_queen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/harley_queen.png -------------------------------------------------------------------------------- /static/hipster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/hipster.png -------------------------------------------------------------------------------- /static/indian_male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/indian_male.png -------------------------------------------------------------------------------- /static/indian_man_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/indian_man_2.png -------------------------------------------------------------------------------- /static/marlin_monroe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/marlin_monroe.png -------------------------------------------------------------------------------- /static/nun_sister.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/nun_sister.png -------------------------------------------------------------------------------- /static/ozzy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/ozzy.png -------------------------------------------------------------------------------- /static/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/pencil.png -------------------------------------------------------------------------------- /static/pilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/pilot.png -------------------------------------------------------------------------------- /static/punk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/punk.png -------------------------------------------------------------------------------- /static/santa_claus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/santa_claus.png -------------------------------------------------------------------------------- /static/sheep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/sheep.png -------------------------------------------------------------------------------- /static/sluggard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/sluggard.png -------------------------------------------------------------------------------- /static/spider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/spider.png -------------------------------------------------------------------------------- /static/trump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/trump.png -------------------------------------------------------------------------------- /static/ufo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/ufo.png -------------------------------------------------------------------------------- /static/wrestler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/heroes-api/771ff725a8620291596b9571bd98401548baec8a/static/wrestler.png -------------------------------------------------------------------------------- /test/heroses.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from '../src/app.module'; 5 | import { HeroesRepository, HeroesService } from '../src/heroes'; 6 | import { heroesMock, heroMock } from './mocks/hero.mock'; 7 | 8 | describe('Heroes (e2e)', () => { 9 | const heroesService: Partial = { 10 | count: async () => heroesMock.length, 11 | heroes: async () => heroesMock, 12 | hero: async () => heroMock, 13 | delete: async () => heroMock, 14 | create: async () => heroMock, 15 | update: async () => heroMock, 16 | random: async () => heroMock, 17 | }; 18 | 19 | let app: INestApplication; 20 | 21 | beforeAll(async () => { 22 | const moduleRef = await Test.createTestingModule({ 23 | imports: [AppModule], 24 | }) 25 | .overrideProvider(HeroesService) 26 | .useValue(heroesService) 27 | .compile(); 28 | 29 | app = moduleRef.createNestApplication(); 30 | await app.init(); 31 | }); 32 | 33 | it('/heroes (GET)', async () => { 34 | const response = await request(app.getHttpServer()).get('/heroes'); 35 | 36 | expect(response.statusCode).toBe(200); 37 | expect(response.body).toEqual({ 38 | data: await heroesService.heroes(), 39 | totalCount: await heroesService.count(), 40 | }); 41 | }); 42 | 43 | it('/heroes/:id (GET)', async () => { 44 | const response = await request(app.getHttpServer()).get('/heroes/1234'); 45 | 46 | expect(response.statusCode).toBe(200); 47 | expect(response.body).toStrictEqual(await heroesService.hero()); 48 | }); 49 | 50 | it('/heroes (POST)', async () => { 51 | const hero = { 52 | fullName: heroMock.fullName, 53 | avatarUrl: heroMock.avatarUrl, 54 | description: heroMock.description, 55 | typeId: heroMock.type.id, 56 | }; 57 | const response = await request(app.getHttpServer()) 58 | .post('/heroes') 59 | .send(hero); 60 | 61 | expect(response.statusCode).toBe(201); 62 | expect(response.body).toStrictEqual(heroMock); 63 | }); 64 | 65 | afterAll(async () => { 66 | await app.close(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/mocks/hero.mock.ts: -------------------------------------------------------------------------------- 1 | import { HeroDto } from '../../src/heroes/dtos'; 2 | import { datatype, image, lorem } from 'faker'; 3 | 4 | export const createHeroMock = (): HeroDto => ({ 5 | id: datatype.uuid(), 6 | avatarUrl: image.avatar(), 7 | description: lorem.sentence(), 8 | type: { id: datatype.uuid(), name: lorem.sentence() }, 9 | fullName: lorem.sentence(1), 10 | }); 11 | 12 | export const heroMock = createHeroMock(); 13 | 14 | export const heroesMock = Array(10) 15 | .fill('') 16 | .map(() => createHeroMock()); 17 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 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 | --------------------------------------------------------------------------------