├── .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 |
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 |
--------------------------------------------------------------------------------