├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── docker-compose.yml
├── nest-cli.json
├── package.json
├── src
├── app.module.ts
├── auth
│ ├── auth.controller.ts
│ ├── auth.module.ts
│ ├── auth.service.ts
│ ├── constants
│ │ └── jwt.constant.ts
│ ├── decorators
│ │ ├── auth.decorator.ts
│ │ └── roles.decorator.ts
│ ├── dto
│ │ ├── login.dto.ts
│ │ └── register.dto.ts
│ └── guard
│ │ ├── auth.guard.ts
│ │ └── roles.guard.ts
├── breeds
│ ├── breeds.controller.ts
│ ├── breeds.module.ts
│ ├── breeds.service.ts
│ ├── dto
│ │ ├── create-breed.dto.ts
│ │ └── update-breed.dto.ts
│ └── entities
│ │ └── breed.entity.ts
├── cats
│ ├── cats.controller.ts
│ ├── cats.module.ts
│ ├── cats.service.ts
│ ├── dto
│ │ ├── create-cat.dto.ts
│ │ └── update-cat.dto.ts
│ └── entities
│ │ └── cat.entity.ts
├── common
│ ├── decorators
│ │ └── active-user.decorator.ts
│ ├── enums
│ │ └── rol.enum.ts
│ └── interfaces
│ │ └── user-active.interface.ts
├── main.ts
└── users
│ ├── dto
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
│ ├── entities
│ └── user.entity.ts
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir: __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | // 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 | mysql
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | pnpm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 |
15 | # OS
16 | .DS_Store
17 |
18 | # Tests
19 | /coverage
20 | /.nyc_output
21 |
22 | # IDEs and editors
23 | /.idea
24 | .project
25 | .classpath
26 | .c9/
27 | *.launch
28 | .settings/
29 | *.sublime-workspace
30 |
31 | # IDE - VSCode
32 | .vscode/*
33 | !.vscode/settings.json
34 | !.vscode/tasks.json
35 | !.vscode/launch.json
36 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 | ## Description
26 |
27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
28 |
29 | ## Installation
30 |
31 | ```bash
32 | $ yarn install
33 | ```
34 |
35 | ## Running the app
36 |
37 | ```bash
38 | # development
39 | $ yarn run start
40 |
41 | # watch mode
42 | $ yarn run start:dev
43 |
44 | # production mode
45 | $ yarn run start:prod
46 | ```
47 |
48 | ## Test
49 |
50 | ```bash
51 | # unit tests
52 | $ yarn run test
53 |
54 | # e2e tests
55 | $ yarn run test:e2e
56 |
57 | # test coverage
58 | $ yarn run test:cov
59 | ```
60 |
61 | ## Support
62 |
63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
64 |
65 | ## Stay in touch
66 |
67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
68 | - Website - [https://nestjs.com](https://nestjs.com/)
69 | - Twitter - [@nestframework](https://twitter.com/nestframework)
70 |
71 | ## License
72 |
73 | Nest is [MIT licensed](LICENSE).
74 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | mysql:
4 | image: mysql:8.0
5 | container_name: mysql_db
6 | restart: always
7 | environment:
8 | MYSQL_ROOT_PASSWORD: root
9 | MYSQL_DATABASE: db_crud
10 | MYSQL_USER: user_crud
11 | MYSQL_PASSWORD: root
12 | volumes:
13 | - ./mysql:/var/lib/mysql
14 | ports:
15 | - "3307:3306"
16 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src",
5 | "compilerOptions": {
6 | "deleteOutDir": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "02-crud-mysql",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "build": "nest build",
10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11 | "start": "nest start",
12 | "start:dev": "nest start --watch",
13 | "start:debug": "nest start --debug --watch",
14 | "start:prod": "node dist/main",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "test": "jest",
17 | "test:watch": "jest --watch",
18 | "test:cov": "jest --coverage",
19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20 | "test:e2e": "jest --config ./test/jest-e2e.json"
21 | },
22 | "dependencies": {
23 | "@nestjs/common": "^10.0.0",
24 | "@nestjs/core": "^10.0.0",
25 | "@nestjs/jwt": "10.1.0",
26 | "@nestjs/mapped-types": "*",
27 | "@nestjs/platform-express": "^10.0.0",
28 | "@nestjs/typeorm": "10.0.0",
29 | "bcryptjs": "2.4.3",
30 | "class-transformer": "0.5.1",
31 | "class-validator": "0.14.0",
32 | "mysql2": "3.5.2",
33 | "reflect-metadata": "^0.1.13",
34 | "rxjs": "^7.8.1",
35 | "typeorm": "0.3.17"
36 | },
37 | "devDependencies": {
38 | "@nestjs/cli": "^10.0.0",
39 | "@nestjs/schematics": "^10.0.0",
40 | "@nestjs/testing": "^10.0.0",
41 | "@types/express": "^4.17.17",
42 | "@types/jest": "^29.5.2",
43 | "@types/node": "^20.3.1",
44 | "@types/supertest": "^2.0.12",
45 | "@typescript-eslint/eslint-plugin": "^5.59.11",
46 | "@typescript-eslint/parser": "^5.59.11",
47 | "eslint": "^8.42.0",
48 | "eslint-config-prettier": "^8.8.0",
49 | "eslint-plugin-prettier": "^4.2.1",
50 | "jest": "^29.5.0",
51 | "prettier": "^2.8.8",
52 | "source-map-support": "^0.5.21",
53 | "supertest": "^6.3.3",
54 | "ts-jest": "^29.1.0",
55 | "ts-loader": "^9.4.3",
56 | "ts-node": "^10.9.1",
57 | "tsconfig-paths": "^4.2.0",
58 | "typescript": "^5.1.3"
59 | },
60 | "jest": {
61 | "moduleFileExtensions": [
62 | "js",
63 | "json",
64 | "ts"
65 | ],
66 | "rootDir": "src",
67 | "testRegex": ".*\\.spec\\.ts$",
68 | "transform": {
69 | "^.+\\.(t|j)s$": "ts-jest"
70 | },
71 | "collectCoverageFrom": [
72 | "**/*.(t|j)s"
73 | ],
74 | "coverageDirectory": "../coverage",
75 | "testEnvironment": "node"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { BreedsModule } from './breeds/breeds.module';
4 | import { CatsModule } from './cats/cats.module';
5 | import { UsersModule } from './users/users.module';
6 | import { AuthModule } from './auth/auth.module';
7 |
8 | @Module({
9 | imports: [
10 | TypeOrmModule.forRoot({
11 | type: 'mysql',
12 | host: 'localhost',
13 | port: 3307,
14 | username: 'user_crud',
15 | password: 'root',
16 | database: 'db_crud',
17 | autoLoadEntities: true,
18 | synchronize: true,
19 | }),
20 | CatsModule,
21 | BreedsModule,
22 | UsersModule,
23 | AuthModule,
24 | ],
25 | controllers: [],
26 | providers: [],
27 | })
28 | export class AppModule {}
29 |
--------------------------------------------------------------------------------
/src/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Get, Post } from '@nestjs/common';
2 | import { Request } from 'express';
3 | import { ActiveUser } from 'src/common/decorators/active-user.decorator';
4 | import { UserActiveInterface } from 'src/common/interfaces/user-active.interface';
5 | import { Role } from '../common/enums/rol.enum';
6 | import { AuthService } from './auth.service';
7 | import { Auth } from './decorators/auth.decorator';
8 | import { LoginDto } from './dto/login.dto';
9 | import { RegisterDto } from './dto/register.dto';
10 |
11 | interface RequestWithUser extends Request {
12 | user: {
13 | email: string;
14 | role: string;
15 | };
16 | }
17 |
18 | @Controller('auth')
19 | export class AuthController {
20 | constructor(private readonly authService: AuthService) {}
21 |
22 | @Post('register')
23 | register(
24 | @Body()
25 | registerDto: RegisterDto,
26 | ) {
27 | return this.authService.register(registerDto);
28 | }
29 |
30 | @Post('login')
31 | login(
32 | @Body()
33 | loginDto: LoginDto,
34 | ) {
35 | return this.authService.login(loginDto);
36 | }
37 |
38 | @Get('profile')
39 | @Auth(Role.USER)
40 | profile(@ActiveUser() user: UserActiveInterface) {
41 | console.log(user)
42 | return this.authService.profile(user);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { JwtModule } from '@nestjs/jwt';
3 | import { UsersModule } from 'src/users/users.module';
4 | import { AuthController } from './auth.controller';
5 | import { AuthService } from './auth.service';
6 | import { jwtConstants } from './constants/jwt.constant';
7 |
8 | @Module({
9 | imports: [
10 | UsersModule,
11 | JwtModule.register({
12 | global: true,
13 | secret: jwtConstants.secret,
14 | signOptions: { expiresIn: '1d' },
15 | }),
16 | ],
17 | controllers: [AuthController],
18 | providers: [AuthService],
19 | })
20 | export class AuthModule {}
21 |
--------------------------------------------------------------------------------
/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BadRequestException,
3 | Injectable,
4 | UnauthorizedException,
5 | } from '@nestjs/common';
6 | import { UsersService } from 'src/users/users.service';
7 | import { RegisterDto } from './dto/register.dto';
8 |
9 | import { JwtService } from '@nestjs/jwt';
10 | import * as bcryptjs from 'bcryptjs';
11 | import { LoginDto } from './dto/login.dto';
12 |
13 | @Injectable()
14 | export class AuthService {
15 | constructor(
16 | private readonly usersService: UsersService,
17 | private readonly jwtService: JwtService,
18 | ) {}
19 |
20 | async register({ name, email, password }: RegisterDto) {
21 | const user = await this.usersService.findOneByEmail(email);
22 |
23 | if (user) {
24 | throw new BadRequestException('User already exists');
25 | }
26 |
27 | await this.usersService.create({
28 | name,
29 | email,
30 | password: await bcryptjs.hash(password, 10),
31 | });
32 |
33 | return {
34 | name,
35 | email,
36 | };
37 | }
38 |
39 | async login({ email, password }: LoginDto) {
40 | const user = await this.usersService.findByEmailWithPassword(email);
41 | if (!user) {
42 | throw new UnauthorizedException('email is wrong');
43 | }
44 |
45 | const isPasswordValid = await bcryptjs.compare(password, user.password);
46 | if (!isPasswordValid) {
47 | throw new UnauthorizedException('password is wrong');
48 | }
49 |
50 | const payload = { email: user.email, role: user.role };
51 | const token = await this.jwtService.signAsync(payload);
52 |
53 | return {
54 | token,
55 | email,
56 | };
57 | }
58 |
59 | async profile({ email, role }: { email: string; role: string }) {
60 | return await this.usersService.findOneByEmail(email);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/auth/constants/jwt.constant.ts:
--------------------------------------------------------------------------------
1 | export const jwtConstants = {
2 | secret: 'no utilizar esta palabra en producción',
3 | };
4 |
--------------------------------------------------------------------------------
/src/auth/decorators/auth.decorator.ts:
--------------------------------------------------------------------------------
1 | import { applyDecorators, UseGuards } from '@nestjs/common';
2 | import { Role } from '../../common/enums/rol.enum';
3 | import { AuthGuard } from '../guard/auth.guard';
4 | import { RolesGuard } from '../guard/roles.guard';
5 | import { Roles } from './roles.decorator';
6 |
7 | export function Auth(role: Role) {
8 | return applyDecorators(Roles(role), UseGuards(AuthGuard, RolesGuard));
9 | }
10 |
--------------------------------------------------------------------------------
/src/auth/decorators/roles.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 | import { Role } from '../../common/enums/rol.enum';
3 |
4 | export const ROLES_KEY = 'roles';
5 | export const Roles = (role: Role) => SetMetadata(ROLES_KEY, role);
6 |
--------------------------------------------------------------------------------
/src/auth/dto/login.dto.ts:
--------------------------------------------------------------------------------
1 | import { Transform } from 'class-transformer';
2 | import { IsEmail, IsString, MinLength } from 'class-validator';
3 |
4 | export class LoginDto {
5 | @IsEmail()
6 | email: string;
7 |
8 | @Transform(({ value }) => value.trim())
9 | @IsString()
10 | @MinLength(6)
11 | password: string;
12 | }
13 |
--------------------------------------------------------------------------------
/src/auth/dto/register.dto.ts:
--------------------------------------------------------------------------------
1 | import { Transform } from 'class-transformer';
2 | import { IsEmail, IsString, MinLength } from 'class-validator';
3 |
4 | export class RegisterDto {
5 | @Transform(({ value }) => value.trim())
6 | @IsString()
7 | @MinLength(1)
8 | name: string;
9 |
10 | @IsEmail()
11 | email: string;
12 |
13 | @Transform(({ value }) => value.trim())
14 | @IsString()
15 | @MinLength(6)
16 | password: string;
17 | }
18 |
--------------------------------------------------------------------------------
/src/auth/guard/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CanActivate,
3 | ExecutionContext,
4 | Injectable,
5 | UnauthorizedException,
6 | } from '@nestjs/common';
7 | import { JwtService } from '@nestjs/jwt';
8 | import { Request } from 'express';
9 | import { jwtConstants } from '../constants/jwt.constant';
10 |
11 | @Injectable()
12 | export class AuthGuard implements CanActivate {
13 | constructor(private readonly jwtService: JwtService) {}
14 |
15 | async canActivate(context: ExecutionContext): Promise {
16 | const request = context.switchToHttp().getRequest();
17 |
18 | const token = this.extractTokenFromHeader(request);
19 | if (!token) {
20 | throw new UnauthorizedException();
21 | }
22 |
23 | try {
24 | const payload = await this.jwtService.verifyAsync(token, {
25 | secret: jwtConstants.secret,
26 | });
27 | request.user = payload;
28 | } catch {
29 | throw new UnauthorizedException();
30 | }
31 |
32 | return true;
33 | }
34 |
35 | private extractTokenFromHeader(request: Request): string | undefined {
36 | const [type, token] = request.headers.authorization?.split(' ') ?? [];
37 | return type === 'Bearer' ? token : undefined;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/auth/guard/roles.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2 | import { Reflector } from '@nestjs/core';
3 | import { Role } from '../../common/enums/rol.enum';
4 | import { ROLES_KEY } from '../decorators/roles.decorator';
5 |
6 | @Injectable()
7 | export class RolesGuard implements CanActivate {
8 | constructor(private readonly reflector: Reflector) {}
9 |
10 | canActivate(context: ExecutionContext): boolean {
11 | const role = this.reflector.getAllAndOverride(ROLES_KEY, [
12 | context.getHandler(),
13 | context.getClass(),
14 | ]);
15 |
16 | if (!role) {
17 | return true;
18 | }
19 |
20 | const { user } = context.switchToHttp().getRequest();
21 |
22 | if(user.role === Role.ADMIN) {
23 | return true;
24 | }
25 |
26 | return role === user.role;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/breeds/breeds.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | Param,
7 | Patch,
8 | Post,
9 | } from '@nestjs/common';
10 | import { Auth } from 'src/auth/decorators/auth.decorator';
11 | import { Role } from 'src/common/enums/rol.enum';
12 | import { BreedsService } from './breeds.service';
13 | import { CreateBreedDto } from './dto/create-breed.dto';
14 | import { UpdateBreedDto } from './dto/update-breed.dto';
15 |
16 | @Auth(Role.ADMIN)
17 | @Controller('breeds')
18 | export class BreedsController {
19 | constructor(private readonly breedsService: BreedsService) {}
20 |
21 | @Post()
22 | create(@Body() createBreedDto: CreateBreedDto) {
23 | return this.breedsService.create(createBreedDto);
24 | }
25 |
26 | @Get()
27 | findAll() {
28 | return this.breedsService.findAll();
29 | }
30 |
31 | @Get(':id')
32 | findOne(@Param('id') id: number) {
33 | return this.breedsService.findOne(id);
34 | }
35 |
36 | @Patch(':id')
37 | update(@Param('id') id: number, @Body() updateBreedDto: UpdateBreedDto) {
38 | return this.breedsService.update(id, updateBreedDto);
39 | }
40 |
41 | @Delete(':id')
42 | remove(@Param('id') id: number) {
43 | return this.breedsService.remove(id);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/breeds/breeds.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { BreedsController } from './breeds.controller';
4 | import { BreedsService } from './breeds.service';
5 | import { Breed } from './entities/breed.entity';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([Breed])],
9 | controllers: [BreedsController],
10 | providers: [BreedsService],
11 | exports: [TypeOrmModule],
12 | })
13 | export class BreedsModule {}
14 |
--------------------------------------------------------------------------------
/src/breeds/breeds.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import { CreateBreedDto } from './dto/create-breed.dto';
5 | import { UpdateBreedDto } from './dto/update-breed.dto';
6 | import { Breed } from './entities/breed.entity';
7 |
8 | @Injectable()
9 | export class BreedsService {
10 | constructor(
11 | @InjectRepository(Breed)
12 | private readonly breedRepository: Repository,
13 | ) {}
14 |
15 | async create(createBreedDto: CreateBreedDto) {
16 | return await this.breedRepository.save(createBreedDto);
17 | }
18 |
19 | async findAll() {
20 | return await this.breedRepository.find();
21 | }
22 |
23 | async findOne(id: number) {
24 | return `This action returns a #${id} breed`;
25 | }
26 |
27 | async update(id: number, updateBreedDto: UpdateBreedDto) {
28 | return `This action updates a #${id} breed`;
29 | }
30 |
31 | async remove(id: number) {
32 | return `This action removes a #${id} breed`;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/breeds/dto/create-breed.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, MinLength } from 'class-validator';
2 |
3 | export class CreateBreedDto {
4 | @IsString()
5 | @MinLength(3)
6 | name: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/breeds/dto/update-breed.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types';
2 | import { CreateBreedDto } from './create-breed.dto';
3 |
4 | export class UpdateBreedDto extends PartialType(CreateBreedDto) {}
5 |
--------------------------------------------------------------------------------
/src/breeds/entities/breed.entity.ts:
--------------------------------------------------------------------------------
1 | import { Cat } from 'src/cats/entities/cat.entity';
2 | import { Column, Entity, OneToMany } from 'typeorm';
3 |
4 | @Entity()
5 | export class Breed {
6 | @Column({ primary: true, generated: true })
7 | id: number;
8 |
9 | @Column({ length: 500 })
10 | name: string;
11 |
12 | @OneToMany(() => Cat, (cat) => cat.breed)
13 | cats: Cat[];
14 | }
15 |
--------------------------------------------------------------------------------
/src/cats/cats.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | Param,
7 | Patch,
8 | Post,
9 | } from '@nestjs/common';
10 | import { Auth } from 'src/auth/decorators/auth.decorator';
11 | import { ActiveUser } from 'src/common/decorators/active-user.decorator';
12 | import { Role } from 'src/common/enums/rol.enum';
13 | import { UserActiveInterface } from 'src/common/interfaces/user-active.interface';
14 | import { CatsService } from './cats.service';
15 | import { CreateCatDto } from './dto/create-cat.dto';
16 | import { UpdateCatDto } from './dto/update-cat.dto';
17 |
18 | @Auth(Role.USER)
19 | @Controller('cats')
20 | export class CatsController {
21 | constructor(private readonly catsService: CatsService) {}
22 |
23 | @Post()
24 | create(@Body() createCatDto: CreateCatDto, @ActiveUser() user: UserActiveInterface) {
25 | return this.catsService.create(createCatDto, user);
26 | }
27 |
28 | @Get()
29 | findAll(@ActiveUser() user: UserActiveInterface) {
30 | return this.catsService.findAll(user);
31 | }
32 |
33 | @Get(':id')
34 | findOne(@Param('id') id: number, @ActiveUser() user: UserActiveInterface) {
35 | return this.catsService.findOne(id, user);
36 | }
37 |
38 | @Patch(':id')
39 | update(@Param('id') id: number, @Body() updateCatDto: UpdateCatDto, @ActiveUser() user: UserActiveInterface) {
40 | return this.catsService.update(id, updateCatDto, user);
41 | }
42 |
43 | @Delete(':id')
44 | remove(@Param('id') id: number, @ActiveUser() user: UserActiveInterface) {
45 | return this.catsService.remove(id, user);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/cats/cats.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { BreedsModule } from 'src/breeds/breeds.module';
4 | import { BreedsService } from 'src/breeds/breeds.service';
5 | import { CatsController } from './cats.controller';
6 | import { CatsService } from './cats.service';
7 | import { Cat } from './entities/cat.entity';
8 |
9 | @Module({
10 | imports: [TypeOrmModule.forFeature([Cat]), BreedsModule],
11 | controllers: [CatsController],
12 | providers: [CatsService, BreedsService],
13 | })
14 | export class CatsModule {}
15 |
--------------------------------------------------------------------------------
/src/cats/cats.service.ts:
--------------------------------------------------------------------------------
1 | import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import { Breed } from '../breeds/entities/breed.entity';
5 | import { Role } from '../common/enums/rol.enum';
6 | import { UserActiveInterface } from '../common/interfaces/user-active.interface';
7 | import { CreateCatDto } from './dto/create-cat.dto';
8 | import { UpdateCatDto } from './dto/update-cat.dto';
9 | import { Cat } from './entities/cat.entity';
10 |
11 | @Injectable()
12 | export class CatsService {
13 | constructor(
14 | @InjectRepository(Cat)
15 | private readonly catRepository: Repository,
16 | @InjectRepository(Breed)
17 | private readonly breedRepository: Repository,
18 | ) {}
19 |
20 | async create(createCatDto: CreateCatDto, user: UserActiveInterface) {
21 | const breed = await this.validateBreed(createCatDto.breed);
22 | return await this.catRepository.save({
23 | ...createCatDto,
24 | breed: breed,
25 | userEmail: user.email,
26 | });
27 | }
28 |
29 | async findAll(user: UserActiveInterface) {
30 | if(user.role === Role.ADMIN) {
31 | return await this.catRepository.find();
32 | }
33 | return await this.catRepository.find({
34 | where: { userEmail: user.email },
35 | });
36 | }
37 |
38 | async findOne(id: number, user: UserActiveInterface) {
39 | const cat = await this.catRepository.findOneBy({ id });
40 | if (!cat) {
41 | throw new BadRequestException('Cat not found');
42 | }
43 | this.validateOwnership(cat, user);
44 | return cat;
45 | }
46 |
47 | async update(id: number, updateCatDto: UpdateCatDto, user: UserActiveInterface) {
48 | await this.findOne(id, user );
49 | return await this.catRepository.update(id, {
50 | ...updateCatDto,
51 | breed: updateCatDto.breed ? await this.validateBreed(updateCatDto.breed) : undefined,
52 | userEmail: user.email,
53 | })
54 | }
55 |
56 | async remove(id: number, user: UserActiveInterface) {
57 | await this.findOne(id, user );
58 | return await this.catRepository.softDelete({ id }); // se le pasa el id
59 | }
60 |
61 | private validateOwnership(cat: Cat, user: UserActiveInterface) {
62 | if (user.role !== Role.ADMIN && cat.userEmail !== user.email) {
63 | throw new UnauthorizedException();
64 | }
65 | }
66 |
67 | private async validateBreed(breed: string) {
68 | const breedEntity = await this.breedRepository.findOneBy({ name: breed });
69 |
70 | if (!breedEntity) {
71 | throw new BadRequestException('Breed not found');
72 | }
73 |
74 | return breedEntity;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/cats/dto/create-cat.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsInt,
3 | IsOptional,
4 | IsPositive,
5 | IsString,
6 | MinLength,
7 | } from 'class-validator';
8 |
9 | export class CreateCatDto {
10 | @IsString()
11 | @MinLength(1)
12 | name: string;
13 |
14 | @IsInt()
15 | @IsPositive()
16 | age: number;
17 |
18 | @IsString()
19 | @IsOptional()
20 | breed?: string;
21 | }
22 |
--------------------------------------------------------------------------------
/src/cats/dto/update-cat.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsInt,
3 | IsOptional,
4 | IsPositive,
5 | IsString,
6 | MinLength,
7 | } from 'class-validator';
8 |
9 | export class UpdateCatDto {
10 | @IsString()
11 | @MinLength(1)
12 | @IsOptional()
13 | name?: string;
14 |
15 | @IsInt()
16 | @IsPositive()
17 | @IsOptional()
18 | age?: number;
19 |
20 | @IsString()
21 | @IsOptional()
22 | breed?: string;
23 | }
24 |
--------------------------------------------------------------------------------
/src/cats/entities/cat.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, DeleteDateColumn, Entity, JoinColumn, ManyToOne } from 'typeorm';
2 | import { Breed } from '../../breeds/entities/breed.entity';
3 | import { User } from '../../users/entities/user.entity';
4 |
5 | @Entity()
6 | export class Cat {
7 | // @PrimaryGeneratedColumn()
8 | @Column({ primary: true, generated: true })
9 | id: number;
10 |
11 | @Column()
12 | name: string;
13 |
14 | @Column()
15 | age: number;
16 |
17 | @DeleteDateColumn()
18 | deletedAt: Date;
19 |
20 | @ManyToOne(() => Breed, (breed) => breed.id, {
21 | eager: true, // para que traiga las raza al hacer un findOne
22 | })
23 | breed: Breed;
24 |
25 | @ManyToOne(() => User)
26 | @JoinColumn({ name: 'userEmail', referencedColumnName: 'email', })
27 | user: User;
28 |
29 | @Column()
30 | userEmail: string;
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/common/decorators/active-user.decorator.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common';
2 |
3 | export const ActiveUser = createParamDecorator(
4 | (data: unknown, ctx: ExecutionContext) => {
5 | const request = ctx.switchToHttp().getRequest();
6 | return request.user
7 | }
8 | )
--------------------------------------------------------------------------------
/src/common/enums/rol.enum.ts:
--------------------------------------------------------------------------------
1 | export enum Role {
2 | USER = 'user',
3 | ADMIN = 'admin',
4 | }
5 |
--------------------------------------------------------------------------------
/src/common/interfaces/user-active.interface.ts:
--------------------------------------------------------------------------------
1 | export interface UserActiveInterface {
2 | email: string;
3 | role: string;
4 | }
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ValidationPipe } from '@nestjs/common';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app.module';
4 |
5 | async function bootstrap() {
6 | const app = await NestFactory.create(AppModule);
7 |
8 | app.setGlobalPrefix('api/v1');
9 |
10 | app.useGlobalPipes(
11 | new ValidationPipe({
12 | whitelist: true,
13 | forbidNonWhitelisted: true,
14 | transform: true,
15 | }),
16 | );
17 |
18 | await app.listen(3000);
19 | }
20 | bootstrap();
21 |
--------------------------------------------------------------------------------
/src/users/dto/create-user.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateUserDto {
2 | email: string;
3 | password: string;
4 | name?: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/users/dto/update-user.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/mapped-types';
2 | import { CreateUserDto } from './create-user.dto';
3 |
4 | export class UpdateUserDto extends PartialType(CreateUserDto) {}
5 |
--------------------------------------------------------------------------------
/src/users/entities/user.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | DeleteDateColumn,
4 | Entity,
5 | PrimaryGeneratedColumn,
6 | } from 'typeorm';
7 | import { Role } from '../../common/enums/rol.enum';
8 |
9 | @Entity()
10 | export class User {
11 | @PrimaryGeneratedColumn()
12 | id: number;
13 |
14 | @Column()
15 | name: string;
16 |
17 | @Column({ unique: true, nullable: false })
18 | email: string;
19 |
20 | @Column({ nullable: false, select: false })
21 | password: string;
22 |
23 | @Column({ type: 'enum', default: Role.USER, enum: Role })
24 | role: Role;
25 |
26 | @DeleteDateColumn()
27 | deletedAt: Date;
28 | }
29 |
--------------------------------------------------------------------------------
/src/users/users.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
2 | import { Auth } from 'src/auth/decorators/auth.decorator';
3 | import { Role } from 'src/common/enums/rol.enum';
4 | import { CreateUserDto } from './dto/create-user.dto';
5 | import { UpdateUserDto } from './dto/update-user.dto';
6 | import { UsersService } from './users.service';
7 |
8 | @Auth(Role.ADMIN)
9 | @Controller('users')
10 | export class UsersController {
11 | constructor(private readonly usersService: UsersService) {}
12 |
13 | @Post()
14 | create(@Body() createUserDto: CreateUserDto) {
15 | return this.usersService.create(createUserDto);
16 | }
17 |
18 | @Get()
19 | findAll() {
20 | return this.usersService.findAll();
21 | }
22 |
23 | @Get(':id')
24 | findOne(@Param('id') id: string) {
25 | return this.usersService.findOne(+id);
26 | }
27 |
28 | @Patch(':id')
29 | update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
30 | return this.usersService.update(+id, updateUserDto);
31 | }
32 |
33 | @Delete(':id')
34 | remove(@Param('id') id: string) {
35 | return this.usersService.remove(+id);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { User } from './entities/user.entity';
4 | import { UsersController } from './users.controller';
5 | import { UsersService } from './users.service';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([User])],
9 | controllers: [UsersController],
10 | providers: [UsersService],
11 | exports: [UsersService],
12 | })
13 | export class UsersModule {}
14 |
--------------------------------------------------------------------------------
/src/users/users.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import { CreateUserDto } from './dto/create-user.dto';
5 | import { UpdateUserDto } from './dto/update-user.dto';
6 | import { User } from './entities/user.entity';
7 |
8 | @Injectable()
9 | export class UsersService {
10 | constructor(
11 | @InjectRepository(User)
12 | private readonly userRepository: Repository,
13 | ) {}
14 |
15 | create(createUserDto: CreateUserDto) {
16 | return this.userRepository.save(createUserDto);
17 | }
18 |
19 | findOneByEmail(email: string) {
20 | return this.userRepository.findOneBy({ email });
21 | }
22 |
23 | findByEmailWithPassword(email: string) {
24 | return this.userRepository.findOne({
25 | where: { email },
26 | select: ['id', 'name', 'email', 'password', 'role'],
27 | });
28 | }
29 |
30 | findAll() {
31 | return this.userRepository.find();
32 | }
33 |
34 | findOne(id: number) {
35 | return `This action returns a #${id} user`;
36 | }
37 |
38 | update(id: number, updateUserDto: UpdateUserDto) {
39 | return `This action updates a #${id} user`;
40 | }
41 |
42 | remove(id: number) {
43 | return `This action removes a #${id} user`;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "ES2021",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "strictNullChecks": false,
16 | "noImplicitAny": false,
17 | "strictBindCallApply": false,
18 | "forceConsistentCasingInFileNames": false,
19 | "noFallthroughCasesInSwitch": false
20 | }
21 | }
22 |
--------------------------------------------------------------------------------