;
3 | }
4 |
--------------------------------------------------------------------------------
/src/auth/domain/ports/primary/http/signin.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IUsersSignInController {
2 | signin(body: P): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/auth/domain/ports/primary/http/signout.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ISignOutController {
2 | whoami(request): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/auth/domain/ports/primary/http/signup.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IUsersSignUpController {
2 | signup(body: P): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/auth/domain/ports/primary/http/whoami.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IWhoAmIController {
2 | whoami(request): R;
3 | }
4 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/refresh-token/dto/refresh-token.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract';
2 | import { Expose } from 'class-transformer';
3 |
4 | export class RefreshTokenResponseDto extends ResponseBaseDto {
5 | @Expose()
6 | token: string;
7 |
8 | @Expose()
9 | refreshToken: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/refresh-token/refresh-token.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { RefreshTokenController } from './refresh-token.controller';
3 | import { RefreshTokenService } from '@auth/application/refresh-token/refresh-token.service';
4 |
5 | describe('RefreshTokenController', () => {
6 | let controller: RefreshTokenController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [RefreshTokenController],
11 | providers: [
12 | {
13 | provide: RefreshTokenService,
14 | useValue: { refreshTokens: async () => ({ token: '', refreshToken: '' }) },
15 | },
16 | ],
17 | }).compile();
18 |
19 | controller = module.get(RefreshTokenController);
20 | });
21 |
22 | it('should be defined', () => {
23 | expect(controller).toBeDefined();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/refresh-token/refresh-token.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
2 | import { IRefreshTokenController } from '@auth/domain/ports/primary/http/refresh-token.controller.interface';
3 | import { ValidateRefTokenAndNewTokens } from '@auth/infrastructure/decorators/new-refresh-token.decorator';
4 | import { CurrentUser } from '@shared/infrastructure/decorators/current-user.decorator';
5 | import { RefreshTokenResponseDto } from './dto/refresh-token.response.dto';
6 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
7 | import { RefreshTokenService } from '@auth/application/refresh-token/refresh-token.service';
8 |
9 | @Controller('auth')
10 | export class RefreshTokenController implements IRefreshTokenController {
11 | constructor(private refreshTokenService: RefreshTokenService) {}
12 |
13 | @Post('/refresh-token')
14 | @ValidateRefTokenAndNewTokens()
15 | @HttpCode(HttpStatus.OK)
16 | @SerializeResponseDto(RefreshTokenResponseDto)
17 | async refreshToken(@CurrentUser() user): Promise {
18 | //el guard coge el token de la cabecera y al user del token y lo mete en el request
19 | return this.refreshTokenService.refreshTokens(user.email, user.refreshToken);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/refresh-token/request.http:
--------------------------------------------------------------------------------
1 |
2 | POST http://{{host}}/api/{{version}}/auth/refresh-token HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{refreshToken}}
5 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/signout/request.http:
--------------------------------------------------------------------------------
1 |
2 | POST http://{{host}}/api/{{version}}/auth/signout HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/signout/signout.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { SignOutController } from './signout.controller';
3 |
4 | describe('SignOutController', () => {
5 | let controller: SignOutController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [SignOutController],
10 | providers: [],
11 | }).compile();
12 |
13 | controller = module.get(SignOutController);
14 | });
15 |
16 | it('should be defined', () => {
17 | expect(controller).toBeDefined();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/signout/signout.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, HttpCode, HttpStatus, Post, Request } from '@nestjs/common';
2 | import { ISignOutController } from '@auth/domain/ports/primary/http/signout.controller.interface';
3 |
4 | @Controller('auth')
5 | export class SignOutController implements ISignOutController {
6 | @Post('/signout')
7 | @HttpCode(HttpStatus.OK)
8 | async whoami(@Request() request): Promise {
9 | request.user = null; // TODO: Implementar lógica de signout, borrar el refresh token de la base de datos
10 | return { ok: true };
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/signup/dto/signup.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
2 |
3 | export class SignUpRequestDto {
4 | @IsNotEmpty()
5 | @IsEmail()
6 | email: string;
7 |
8 | @IsNotEmpty()
9 | @IsString()
10 | password: string;
11 | }
12 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/signup/dto/user.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract';
2 | import { Expose } from 'class-transformer';
3 |
4 | export class UserResponseDto extends ResponseBaseDto {
5 | @Expose()
6 | id: number;
7 |
8 | @Expose()
9 | email: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/signup/request.http:
--------------------------------------------------------------------------------
1 |
2 | POST http://{{host}}/api/{{version}}/auth/signup HTTP/1.1
3 | Content-Type: {{contentType}}
4 |
5 | {
6 | "email": "yasniel@gmail.com",
7 | "password": "yasniel"
8 | }
9 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/signup/signup.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
2 | import { SignUpRequestDto } from './dto/signup.request.dto';
3 | import { IUsersSignUpController } from '@auth/domain/ports/primary/http/signup.controller.interface';
4 | import { UserResponseDto } from './dto/user.response.dto';
5 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
6 | import { SignUpService } from '@auth/application/signup/signup.service';
7 | import { Public } from '@shared/infrastructure/decorators/public.decorator';
8 |
9 | @Controller('auth')
10 | export class SingUpController implements IUsersSignUpController {
11 | constructor(private singUpService: SignUpService) {}
12 |
13 | @Public()
14 | @Post('/signup')
15 | @HttpCode(HttpStatus.CREATED)
16 | @SerializeResponseDto(UserResponseDto)
17 | async signup(@Body() body: SignUpRequestDto): Promise {
18 | const user: UserResponseDto = await this.singUpService.signup(body.email, body.password);
19 | return user;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/signup/singup.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { SingUpController } from './signup.controller';
3 | import { SignUpService } from '@auth/application/signup/signup.service';
4 |
5 | describe('UsersController', () => {
6 | let controller: SingUpController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [SingUpController],
11 | providers: [{ provide: SignUpService, useValue: jest.mock }],
12 | }).compile();
13 |
14 | controller = module.get(SingUpController);
15 | });
16 |
17 | it('should be defined', () => {
18 | expect(controller).toBeDefined();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/singin/dto/signin.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
2 |
3 | export class SignInRequestDto {
4 | @IsNotEmpty()
5 | @IsEmail()
6 | email: string;
7 |
8 | @IsNotEmpty()
9 | @IsString()
10 | password: string;
11 | }
12 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/singin/dto/signin.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract';
2 | import { Expose } from 'class-transformer';
3 |
4 | export class SignInResponseDto extends ResponseBaseDto {
5 | @Expose()
6 | token: string;
7 |
8 | @Expose()
9 | refreshToken: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/singin/request.http:
--------------------------------------------------------------------------------
1 |
2 | POST http://{{host}}/api/{{version}}/auth/signin HTTP/1.1
3 | Content-Type: {{contentType}}
4 |
5 | {
6 | "email": "yasniel@gmail.com",
7 | "password": "yasniel"
8 | }
9 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/singin/signin.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
2 | import { SignInResponseDto } from './dto/signin.response.dto';
3 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
4 | import { IUsersSignInController } from '@auth/domain/ports/primary/http/signin.controller.interface';
5 |
6 | import { SignInRequestDto } from './dto/signin.request.dto';
7 | import { SignInService } from '@auth/application/signin/signin.service';
8 | import { Public } from '@shared/infrastructure/decorators/public.decorator';
9 |
10 | @Controller('auth')
11 | export class SignInController implements IUsersSignInController {
12 | constructor(private signInService: SignInService) {}
13 |
14 | @Public()
15 | @Post('/signin')
16 | @HttpCode(HttpStatus.OK)
17 | @SerializeResponseDto(SignInResponseDto)
18 | async signin(@Body() body: SignInRequestDto): Promise {
19 | const { token, refreshToken } = await this.signInService.signin(body.email, body.password);
20 | return { token, refreshToken };
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/singin/singin.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { SignInController } from './signin.controller';
3 | import { SignInService } from '@auth/application/signin/signin.service';
4 |
5 | describe('SignInController', () => {
6 | let controller: SignInController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [SignInController],
11 | providers: [
12 | {
13 | provide: SignInService,
14 | useValue: { signin: async () => ({ token: 'token', refreshToken: 'refreshToken' }) },
15 | },
16 | ],
17 | }).compile();
18 |
19 | controller = module.get(SignInController);
20 | });
21 |
22 | it('should be defined', () => {
23 | expect(controller).toBeDefined();
24 | });
25 |
26 | it('should singin user', async () => {
27 | const response = await controller.signin({ email: 'test@gmail.com', password: 'test' });
28 | expect(response).toEqual({ token: 'token', refreshToken: 'refreshToken' });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/whoami/dto/user.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract';
2 | import { Expose } from 'class-transformer';
3 |
4 | export class UserResponseDto extends ResponseBaseDto {
5 | @Expose()
6 | id: number;
7 |
8 | @Expose()
9 | email: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/whoami/request.http:
--------------------------------------------------------------------------------
1 |
2 | POST http://{{host}}/api/{{version}}/auth/whoami HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
5 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/whoami/whoami.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { WhoAmIController } from './whoami.controller';
3 |
4 | describe('WhoAmIController', () => {
5 | let controller: WhoAmIController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [WhoAmIController],
10 | providers: [],
11 | }).compile();
12 |
13 | controller = module.get(WhoAmIController);
14 | });
15 |
16 | it('should be defined', () => {
17 | expect(controller).toBeDefined();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/primary/http/whoami/whoami.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
2 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
3 | import { IWhoAmIController } from '@auth/domain/ports/primary/http/whoami.controller.interface';
4 | import { UserResponseDto } from './dto/user.response.dto';
5 | import { CurrentUser } from '@shared/infrastructure/decorators/current-user.decorator';
6 | @Controller('auth')
7 | export class WhoAmIController implements IWhoAmIController {
8 | @Post('/whoami')
9 | @HttpCode(HttpStatus.OK)
10 | @SerializeResponseDto(UserResponseDto)
11 | whoami(@CurrentUser() user): UserResponseDto {
12 | return user;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/secondary/db/dao/user.dao.ts:
--------------------------------------------------------------------------------
1 | import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity('User')
4 | export class UserDao {
5 | @PrimaryGeneratedColumn()
6 | id: number;
7 |
8 | @Column({ unique: true })
9 | email: string;
10 |
11 | @Column()
12 | password: string;
13 |
14 | @Column({
15 | unique: false,
16 | nullable: true,
17 | })
18 | name?: string;
19 |
20 | @Column({ default: true })
21 | isAdmin: boolean;
22 |
23 | @Column({
24 | nullable: true,
25 | length: 500,
26 | })
27 | refreshToken: string;
28 |
29 | @CreateDateColumn()
30 | createdAt: Date;
31 | }
32 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/adapters/secondary/db/user.repository.ts:
--------------------------------------------------------------------------------
1 | import { Repository, SaveOptions } from 'typeorm';
2 | import { UserDao } from './dao/user.dao';
3 | import { InjectRepository } from '@nestjs/typeorm';
4 | import { Injectable } from '@nestjs/common';
5 | import { IAuthRepositoryInterface } from '@auth/domain/ports/db/user.repository';
6 |
7 | @Injectable()
8 | export class AuthRepository implements IAuthRepositoryInterface {
9 | constructor(
10 | @InjectRepository(UserDao)
11 | private repository: Repository,
12 | ) {}
13 |
14 | create(data: UserDao): UserDao {
15 | return this.repository.create(data);
16 | }
17 |
18 | findByEmail(email: string): Promise {
19 | return this.repository.findOneBy({ email });
20 | }
21 |
22 | save(entity: UserDao, options?: SaveOptions): Promise {
23 | return this.repository.save(entity, options);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/auth-strategies/jwt-strategy.ts:
--------------------------------------------------------------------------------
1 | import { ExtractJwt, Strategy } from 'passport-jwt';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable } from '@nestjs/common';
4 | import { ConfigService } from '@nestjs/config';
5 |
6 | @Injectable()
7 | export class JwtStrategy extends PassportStrategy(Strategy) {
8 | constructor(protected configService: ConfigService) {
9 | super({
10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
11 | ignoreExpiration: false,
12 | secretOrKey: configService.getOrThrow('JWT_KEY'),
13 | });
14 | }
15 |
16 | async validate(payload: any) {
17 | return { id: payload.id, email: payload.email, isAdmin: payload.isAdmin };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/auth-strategies/local-strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { ValidateUserService } from '@auth/application/validate-user/validate-user.service';
4 | import { Strategy } from 'passport-local';
5 |
6 | @Injectable()
7 | export class LocalStrategy extends PassportStrategy(Strategy) {
8 | constructor(private validateUserService: ValidateUserService) {
9 | super();
10 | }
11 |
12 | async validate(email: string, password: string) {
13 | // const user = await this.validateUserService.validate(email, password);
14 | // if (!user) {
15 | // throw new UnauthorizedException();
16 | // }
17 | // return user;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/auth-strategies/refresh-token.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import { PassportStrategy } from '@nestjs/passport';
4 | import { ExtractJwt, Strategy } from 'passport-jwt';
5 | import { Request } from 'express';
6 | @Injectable()
7 | export class RefreshJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
8 | constructor(protected configService: ConfigService) {
9 | super({
10 | // jwtFromRequest: ExtractJwt.fromBodyField('refresh'),
11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
12 | ignoreExpiration: false,
13 | passReqToCallback: true,
14 | secretOrKey: configService.getOrThrow('JWT_KEY'),
15 | });
16 | }
17 |
18 | async validate(req: Request, payload: any) {
19 | const refreshToken = req.get('Authorization').replace('Bearer', '').trim();
20 | return { ...payload, refreshToken };
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/decorators/new-refresh-token.decorator.ts:
--------------------------------------------------------------------------------
1 | import { UseGuards } from '@nestjs/common';
2 | import { RefreshJwtGuard } from '../guards/refresh-jwt-auth.guard';
3 |
4 | export function ValidateRefTokenAndNewTokens(): MethodDecorator & ClassDecorator {
5 | return UseGuards(new RefreshJwtGuard());
6 | }
7 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/guards/is-admin.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext } from '@nestjs/common';
2 |
3 | export class IsAdminGuard implements CanActivate {
4 | canActivate(context: ExecutionContext) {
5 | const request = context.switchToHttp().getRequest();
6 |
7 | if (!request.user) {
8 | return false;
9 | }
10 |
11 | if (request.user.isAdmin) {
12 | return true;
13 | }
14 |
15 | return false;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/guards/jwt-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionContext, Injectable } from '@nestjs/common';
2 | import { Reflector } from '@nestjs/core';
3 | import { AuthGuard } from '@nestjs/passport';
4 | import { IS_PUBLIC_KEY } from '@shared/infrastructure/decorators/public.decorator';
5 |
6 | @Injectable()
7 | export class JwtAuthGuard extends AuthGuard('jwt') {
8 | constructor(private reflector: Reflector) {
9 | super({
10 | passReqToCallback: true,
11 | });
12 | }
13 |
14 | canActivate(context: ExecutionContext) {
15 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [
16 | context.getHandler(),
17 | context.getClass(),
18 | ]);
19 | if (isPublic) {
20 | return true;
21 | }
22 | return super.canActivate(context);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/guards/local-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export class LocalAuthGuard extends AuthGuard('local') {}
6 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/guards/refresh-jwt-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export class RefreshJwtGuard extends AuthGuard('jwt-refresh') {}
6 |
--------------------------------------------------------------------------------
/src/auth/infrastructure/interceptors/current-user.interceptor.ts:
--------------------------------------------------------------------------------
1 | // import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
2 | // import { FindByEmailService } from '@auth/application/services/find-by-email/find-by-email.service';
3 | // import { Observable } from 'rxjs';
4 |
5 | // export class CurrentUserInterceptor implements NestInterceptor {
6 | // constructor(private readonly findByEmailService: FindByEmailService) {}
7 |
8 | // async intercept(context: ExecutionContext, next: CallHandler): Promise> {
9 | // const request = context.switchToHttp().getRequest();
10 | // if (request.user) {
11 | // const user = await this.findByEmailService.find(request.user.email);
12 | // request.user = user;
13 | // }
14 |
15 | // return next.handle();
16 | // }
17 | // }
18 |
--------------------------------------------------------------------------------
/src/config/constants.ts:
--------------------------------------------------------------------------------
1 | export const isProd = process.env.NODE_ENV === 'prod';
2 | export const isDev = process.env.NODE_ENV === 'dev';
3 | export const isQa = process.env.NODE_ENV === 'qa';
4 | export const isTest = process.env.NODE_ENV === 'test';
5 | export const isStaging = process.env.NODE_ENV === 'staging';
6 | export const isLocal = process.env.NODE_ENV === 'local';
7 |
--------------------------------------------------------------------------------
/src/config/db/data-source.ts:
--------------------------------------------------------------------------------
1 | import { DataSource, DataSourceOptions } from 'typeorm';
2 | import * as dotenv from 'dotenv';
3 |
4 | dotenv.config();
5 |
6 | export const dataSourceOptions: DataSourceOptions = {
7 | type: 'mysql',
8 | host: 'localhost',
9 | port: 3306,
10 | username: 'user',
11 | password: 'password',
12 | database: 'db',
13 | entities: ['dist/**/**.dao{.ts,.js}'],
14 | migrations: ['dist/**/**.migration{.ts,.js}'],
15 | };
16 |
17 | const dataSource = new DataSource(dataSourceOptions);
18 | export default dataSource;
19 |
--------------------------------------------------------------------------------
/src/config/db/database.config.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 | import { isLocal, isTest } from '../constants';
3 | import { TypeOrmModuleOptions } from '@nestjs/typeorm';
4 | import { ConfigService } from '@nestjs/config';
5 | import * as dotenv from 'dotenv';
6 |
7 | dotenv.config();
8 |
9 | const typeOrmConfig = (configService: ConfigService) =>
10 | ({
11 | type: configService.getOrThrow('DB_TYPE'),
12 | host: configService.getOrThrow('DB_HOST'),
13 | port: configService.getOrThrow('DB_PORT'),
14 | username: configService.getOrThrow('DB_USERNAME'),
15 | password: configService.getOrThrow('DB_PASSWORD'),
16 | database: configService.getOrThrow('DB_DATABASE'),
17 | entities: [join(__dirname, 'src/**/**.dao{.ts,.js}')],
18 | migrations: [join(__dirname, 'src/**/**.migration{.ts,.js}')],
19 | synchronize: isLocal || isTest ? true : false, // esto solo es para desarrollo, para produccion usamos migraciones
20 | logging: false, // esto es para debugear las consultas a la base de datos
21 | } as TypeOrmModuleOptions);
22 |
23 | export default typeOrmConfig;
24 |
--------------------------------------------------------------------------------
/src/config/db/migration-new-way.ts:
--------------------------------------------------------------------------------
1 | const { MigrationInterface, QueryRunner, Table } = require('typeorm');
2 |
3 | module.exports = class initialSchema1625847615203 {
4 | name = 'initialSchema1625847615203';
5 |
6 | async up(queryRunner) {
7 | await queryRunner.createTable(
8 | new Table({
9 | name: 'user',
10 | columns: [
11 | {
12 | name: 'id',
13 | type: 'integer',
14 | isPrimary: true,
15 | isGenerated: true,
16 | generationStrategy: 'increment',
17 | },
18 | {
19 | name: 'email',
20 | type: 'varchar',
21 | },
22 | {
23 | name: 'password',
24 | type: 'varchar',
25 | },
26 | {
27 | name: 'admin',
28 | type: 'boolean',
29 | default: 'true',
30 | },
31 | ],
32 | }),
33 | );
34 |
35 | await queryRunner.createTable(
36 | new Table({
37 | name: 'report',
38 | columns: [
39 | {
40 | name: 'id',
41 | type: 'integer',
42 | isPrimary: true,
43 | isGenerated: true,
44 | generationStrategy: 'increment',
45 | },
46 | { name: 'approved', type: 'boolean', default: 'false' },
47 | { name: 'price', type: 'float' },
48 | { name: 'make', type: 'varchar' },
49 | { name: 'model', type: 'varchar' },
50 | { name: 'year', type: 'integer' },
51 | { name: 'lng', type: 'float' },
52 | { name: 'lat', type: 'float' },
53 | { name: 'mileage', type: 'integer' },
54 | { name: 'userId', type: 'integer' },
55 | ],
56 | }),
57 | );
58 | }
59 |
60 | async down(queryRunner) {
61 | await queryRunner.query(`DROP TABLE ""report""`);
62 | await queryRunner.query(`DROP TABLE ""user""`);
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/src/config/db/migrations/1692199497672-NewMigration.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner } from 'typeorm';
2 |
3 | export class NewMigration1692199497672 implements MigrationInterface {
4 | name = 'NewMigration1692199497672';
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.query(`DROP INDEX \`IDX_4a257d2c9837248d70640b3e36\` ON \`User\``);
8 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`refreshToken\``);
9 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`updatedAt\``);
10 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`approved\``);
11 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`lat\``);
12 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`lng\``);
13 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`mileage\``);
14 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`model\``);
15 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`name\``);
16 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`model\` varchar(255) NULL`);
17 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`lng\` int NULL`);
18 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`lat\` int NULL`);
19 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`mileage\` int NULL`);
20 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`approved\` tinyint NOT NULL DEFAULT 0`);
21 | await queryRunner.query(`ALTER TABLE \`User\` ADD \`name\` varchar(255) NULL`);
22 | await queryRunner.query(`ALTER TABLE \`User\` ADD \`refreshToken\` varchar(500) NULL`);
23 | await queryRunner.query(
24 | `ALTER TABLE \`User\` ADD \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)`,
25 | );
26 | await queryRunner.query(`ALTER TABLE \`Report\` DROP FOREIGN KEY \`FK_abc8ce53e6ef1567f06400344fd\``);
27 | await queryRunner.query(`ALTER TABLE \`Report\` CHANGE \`userId\` \`userId\` int NOT NULL`);
28 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`isAdmin\` \`isAdmin\` tinyint NOT NULL`);
29 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`isAdmin\` \`isAdmin\` tinyint NOT NULL`);
30 | await queryRunner.query(
31 | `ALTER TABLE \`User\` CHANGE \`createdAt\` \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)`,
32 | );
33 | await queryRunner.query(`ALTER TABLE \`User\` ADD UNIQUE INDEX \`IDX_4a257d2c9837248d70640b3e36\` (\`email\`)`);
34 | await queryRunner.query(
35 | `ALTER TABLE \`User\` CHANGE \`createdAt\` \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)`,
36 | );
37 | await queryRunner.query(
38 | `ALTER TABLE \`Report\` ADD CONSTRAINT \`FK_abc8ce53e6ef1567f06400344fd\` FOREIGN KEY (\`userId\`) REFERENCES \`User\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`,
39 | );
40 | }
41 |
42 | public async down(queryRunner: QueryRunner): Promise {
43 | await queryRunner.query(`ALTER TABLE \`Report\` DROP FOREIGN KEY \`FK_abc8ce53e6ef1567f06400344fd\``);
44 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`createdAt\` \`createdAt\` datetime NOT NULL`);
45 | await queryRunner.query(`ALTER TABLE \`User\` DROP INDEX \`IDX_4a257d2c9837248d70640b3e36\``);
46 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`createdAt\` \`createdAt\` datetime NOT NULL`);
47 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`isAdmin\` \`isAdmin\` tinyint NOT NULL DEFAULT '1'`);
48 | await queryRunner.query(`ALTER TABLE \`User\` CHANGE \`isAdmin\` \`isAdmin\` tinyint NOT NULL DEFAULT '1'`);
49 | await queryRunner.query(`ALTER TABLE \`Report\` CHANGE \`userId\` \`userId\` int NULL`);
50 | await queryRunner.query(
51 | `ALTER TABLE \`Report\` ADD CONSTRAINT \`FK_abc8ce53e6ef1567f06400344fd\` FOREIGN KEY (\`userId\`) REFERENCES \`User\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`,
52 | );
53 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`updatedAt\``);
54 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`refreshToken\``);
55 | await queryRunner.query(`ALTER TABLE \`User\` DROP COLUMN \`name\``);
56 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`approved\``);
57 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`mileage\``);
58 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`lat\``);
59 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`lng\``);
60 | await queryRunner.query(`ALTER TABLE \`Report\` DROP COLUMN \`model\``);
61 | await queryRunner.query(`ALTER TABLE \`User\` ADD \`name\` varchar(255) NULL`);
62 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`model\` varchar(255) NULL`);
63 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`mileage\` int NULL`);
64 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`lng\` int NULL`);
65 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`lat\` int NULL`);
66 | await queryRunner.query(`ALTER TABLE \`Report\` ADD \`approved\` tinyint NOT NULL DEFAULT '0'`);
67 | await queryRunner.query(
68 | `ALTER TABLE \`User\` ADD \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)`,
69 | );
70 | await queryRunner.query(`ALTER TABLE \`User\` ADD \`refreshToken\` varchar(500) NULL`);
71 | await queryRunner.query(`CREATE UNIQUE INDEX \`IDX_4a257d2c9837248d70640b3e36\` ON \`User\` (\`email\`)`);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/config/environments/dev.env:
--------------------------------------------------------------------------------
1 | ENV=dev
2 | PORT=3000
3 | PROJECT_ID=example
4 | CORS_ALLOWED_ORIGIN=*
5 | JWT_KEY=jwtexamplekey
6 | SALT_ROUNDS=10
7 |
8 | DB_TYPE=mysql
9 | DB_HOST=db #service name porque estamos usando docker-compose
10 | DB_PORT=3306
11 | DB_USERNAME=user
12 | DB_PASSWORD=password
13 | DB_DATABASE=db
--------------------------------------------------------------------------------
/src/config/environments/local.env:
--------------------------------------------------------------------------------
1 | ENV=local
2 | PORT=3000
3 | PROJECT_ID=example
4 | CORS_ALLOWED_ORIGIN=*
5 | JWT_KEY=jwtexamplekey
6 | SALT_ROUNDS=10
7 |
8 | DB_TYPE=sqlite
9 | DB_HOST=
10 | DB_PORT=
11 | DB_USERNAME=
12 | DB_PASSWORD=
13 | DB_DATABASE=db.sqlite
--------------------------------------------------------------------------------
/src/config/environments/prod.env:
--------------------------------------------------------------------------------
1 | ENV=prod
2 | PORT=3000
3 | PROJECT_ID=example
4 | CORS_ALLOWED_ORIGIN=https://example.com
5 | JWT_KEY=jwtexamplekey
6 | SALT_ROUNDS=10
7 |
8 | DB_TYPE=mysql
9 | DB_HOST=
10 | DB_PORT=
11 | DB_USERNAME=
12 | DB_PASSWORD=
13 | DB_DATABASE=
--------------------------------------------------------------------------------
/src/config/environments/qa.env:
--------------------------------------------------------------------------------
1 | ENV=test
2 | PORT=3000
3 | PROJECT_ID=example
4 | CORS_ALLOWED_ORIGIN=*
5 | JWT_KEY=jwtexamplekey
6 | SALT_ROUNDS=10
7 |
8 | DB_TYPE=mysql
9 | DB_HOST=
10 | DB_PORT=
11 | DB_USERNAME=
12 | DB_PASSWORD=
13 | DB_DATABASE=
--------------------------------------------------------------------------------
/src/config/environments/staging.env:
--------------------------------------------------------------------------------
1 | ENV=staging
2 | PORT=3000
3 | PROJECT_ID=example
4 | CORS_ALLOWED_ORIGIN=*
5 | JWT_KEY=jwtexamplekey
6 | SALT_ROUNDS=10
7 |
8 | DB_TYPE=mysql
9 | DB_HOST=
10 | DB_PORT=
11 | DB_USERNAME=
12 | DB_PASSWORD=
13 | DB_DATABASE=
--------------------------------------------------------------------------------
/src/config/environments/test.env:
--------------------------------------------------------------------------------
1 | ENV=test
2 | PORT=3000
3 | PROJECT_ID=example
4 | CORS_ALLOWED_ORIGIN=*
5 | JWT_KEY=jwtexamplekey
6 | SALT_ROUNDS=10
7 |
8 | DB_TYPE=sqlite
9 | DB_HOST=
10 | DB_PORT=
11 | DB_USERNAME=
12 | DB_PASSWORD=
13 | DB_DATABASE=:memory:
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { setupApp } from './setup-app';
3 | import { NestFactory } from '@nestjs/core';
4 | import { NestExpressApplication } from '@nestjs/platform-express';
5 | import { AppModule } from '@app/app.module';
6 |
7 | async function bootstrap() {
8 | const app: NestExpressApplication = await NestFactory.create(AppModule);
9 | const configService = app.get(ConfigService);
10 | setupApp(app);
11 | await app.listen(configService.getOrThrow('PORT'));
12 | }
13 | bootstrap();
14 |
--------------------------------------------------------------------------------
/src/reports/application/approved-report/approved-report.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { ApprovedReportService } from './approved-report.service';
3 | import { getRepositoryToken } from '@nestjs/typeorm';
4 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao';
5 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository';
6 |
7 | describe('ApprovedReportService', () => {
8 | let service: ApprovedReportService;
9 | const mockRepository = {
10 | findOneBy: jest.fn().mockImplementation((dao: ReportDao) => {
11 | return Promise.resolve({
12 | id: Math.ceil(Math.random() * 10),
13 | ...dao,
14 | });
15 | }),
16 | };
17 |
18 | beforeEach(async () => {
19 | const moduleRef = await Test.createTestingModule({
20 | imports: [], // Add
21 | controllers: [], // Add
22 | providers: [
23 | ApprovedReportService,
24 | ReportRepository,
25 | {
26 | provide: getRepositoryToken(ReportDao),
27 | useValue: mockRepository,
28 | },
29 | ], // Add
30 | }).compile();
31 |
32 | service = moduleRef.get(ApprovedReportService);
33 | });
34 |
35 | it('should be defined', () => {
36 | expect(service).toBeDefined();
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/reports/application/approved-report/approved-report.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao';
3 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository';
4 | import { Report } from '@src/reports/domain/entity/report';
5 | import { User } from '@src/reports/domain/entity/user';
6 | import { EmailValueObject } from '@src/shared/domain/value-objects/email.value.object';
7 |
8 | @Injectable()
9 | export class ApprovedReportService {
10 | constructor(private reportRepository: ReportRepository) {}
11 |
12 | async changeApproved(id: number, isApproved: boolean): Promise {
13 | const reportDao = await this.reportRepository.findById(id);
14 |
15 | if (!reportDao) {
16 | throw new NotFoundException('Report not found!');
17 | }
18 |
19 | const user = new User(reportDao.user.id, new EmailValueObject(reportDao.user.email), reportDao.user.name);
20 |
21 | const report = new Report(reportDao.price, reportDao.make, reportDao.year);
22 | report.setOptionalFields(reportDao.model, reportDao.lng, reportDao.lat, reportDao.mileage);
23 | report.setUser(user);
24 | report.setApproved(isApproved);
25 |
26 | return await this.reportRepository.save(report.toJSON() as ReportDao);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/reports/application/create-report/create-report.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { CreateReportService } from './create-report.service';
3 | import { getRepositoryToken } from '@nestjs/typeorm';
4 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao';
5 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository';
6 |
7 | describe('CreateReportService', () => {
8 | let service: CreateReportService;
9 | const mockRepository = {
10 | create: jest.fn().mockImplementation((dao: ReportDao) => {
11 | return Promise.resolve({
12 | id: Math.ceil(Math.random() * 10),
13 | ...dao,
14 | });
15 | }),
16 | };
17 |
18 | beforeEach(async () => {
19 | const moduleRef = await Test.createTestingModule({
20 | imports: [], // Add
21 | controllers: [], // Add
22 | providers: [
23 | CreateReportService,
24 | ReportRepository,
25 | {
26 | provide: getRepositoryToken(ReportDao),
27 | useValue: mockRepository,
28 | },
29 | ], // Add
30 | }).compile();
31 |
32 | service = moduleRef.get(CreateReportService);
33 | });
34 |
35 | it('should be defined', () => {
36 | expect(service).toBeDefined();
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/reports/application/create-report/create-report.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao';
3 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository';
4 | import { CreateReportRequestDto } from '@reports/infrastructure/adapters/primary/http/create-report/dto/report.request.dto';
5 | import { Report } from '@reports/domain/entity/report';
6 |
7 | @Injectable()
8 | export class CreateReportService {
9 | constructor(private reportRepository: ReportRepository) {}
10 |
11 | async create(reportDto: CreateReportRequestDto, user): Promise {
12 | const report = new Report(reportDto.price, reportDto.make, reportDto.year);
13 | report.setOptionalFields(reportDto.model, reportDto.lng, reportDto.lat, reportDto.mileage);
14 | report.setUser(user);
15 | report.setApproved(false);
16 |
17 | const reportCreated = this.reportRepository.create(report.toJSON() as ReportDao);
18 | reportCreated.userId = user.userId;
19 | return await this.reportRepository.save(reportCreated);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/reports/application/get-estimate/get-estimate.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { GetEstimateService } from './get-estimate.service';
3 | import { getRepositoryToken } from '@nestjs/typeorm';
4 | import { ReportDao } from '@reports/infrastructure/adapters/secondary/db/dao/report.dao';
5 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository';
6 |
7 | describe('GetEstimateService', () => {
8 | let service: GetEstimateService;
9 | const mockRepository = {
10 | findOneBy: jest.fn().mockImplementation((dao: ReportDao) => {
11 | return Promise.resolve({
12 | id: Math.ceil(Math.random() * 10),
13 | ...dao,
14 | });
15 | }),
16 | };
17 |
18 | beforeEach(async () => {
19 | const moduleRef = await Test.createTestingModule({
20 | imports: [], // Add
21 | controllers: [], // Add
22 | providers: [
23 | GetEstimateService,
24 | ReportRepository,
25 | {
26 | provide: getRepositoryToken(ReportDao),
27 | useValue: mockRepository,
28 | },
29 | ], // Add
30 | }).compile();
31 |
32 | service = moduleRef.get(GetEstimateService);
33 | });
34 |
35 | it('should be defined', () => {
36 | expect(service).toBeDefined();
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/reports/application/get-estimate/get-estimate.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { ReportRepository } from '@reports/infrastructure/adapters/secondary/db/report.repository';
3 | import { GetEstimateRequestDto } from '@reports/infrastructure/adapters/primary/http/get-estimate/dto/get-estimate.request.dto';
4 | import { GetEstimateResponseDto } from '@reports/infrastructure/adapters/primary/http/get-estimate/dto/get-estimate.response.dto';
5 |
6 | @Injectable()
7 | export class GetEstimateService {
8 | constructor(private reportRepository: ReportRepository) {}
9 |
10 | async getEstimate(query: GetEstimateRequestDto): Promise {
11 | const price: GetEstimateResponseDto = await this.reportRepository.getByQueryBuilder(query);
12 | return price;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/reports/domain/entity/report.ts:
--------------------------------------------------------------------------------
1 | import { MileageError } from '../errors/mileage-error';
2 | import { PriceError } from '../errors/price-error';
3 | import { YearError } from '../errors/year-error';
4 | import { User } from './user';
5 |
6 | export class Report {
7 | model?: string;
8 | lng?: number;
9 | lat?: number;
10 | mileage?: number;
11 | approved: boolean;
12 | user: User;
13 |
14 | constructor(private price: number, private make: string, private year: number) {
15 | if (year < 1930 || year > 2050) {
16 | throw new YearError();
17 | }
18 | if (price < 0 || price > 1000000) {
19 | throw new PriceError();
20 | }
21 | }
22 |
23 | setUser(user: User) {
24 | this.user = user;
25 | }
26 |
27 | setApproved(approved: boolean) {
28 | this.approved = approved;
29 | }
30 |
31 | setOptionalFields(model?: string, lng?: number, lat?: number, mileage?: number) {
32 | this.model = model;
33 | this.lng = lng;
34 | this.lat = lat;
35 | if (mileage < 0 || mileage > 1000000) {
36 | throw new MileageError();
37 | }
38 | this.mileage = mileage;
39 | }
40 |
41 | toJSON() {
42 | return {
43 | price: this.price,
44 | make: this.make,
45 | model: this.model,
46 | year: this.year,
47 | lng: this.lng,
48 | lat: this.lat,
49 | mileage: this.mileage,
50 | approved: this.approved,
51 | };
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/reports/domain/entity/user.ts:
--------------------------------------------------------------------------------
1 | import { EmailValueObject } from '@src/shared/domain/value-objects/email.value.object';
2 |
3 | export class User {
4 | constructor(private id: number, private email: EmailValueObject, private name: string) {}
5 |
6 | async toJSON() {
7 | return {
8 | id: this.id,
9 | email: this.email.getValue,
10 | name: this.name,
11 | };
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/reports/domain/errors/mileage-error.ts:
--------------------------------------------------------------------------------
1 | export class MileageError extends Error {
2 | constructor() {
3 | super('Price must be between 0 and 1000000');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/reports/domain/errors/price-error.ts:
--------------------------------------------------------------------------------
1 | export class PriceError extends Error {
2 | constructor() {
3 | super('Price must be between 0 and 1000000');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/reports/domain/errors/year-error.ts:
--------------------------------------------------------------------------------
1 | export class YearError extends Error {
2 | constructor() {
3 | super('Year must be between 1930 and 2050');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/reports/domain/ports/primary/http/approved-report.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IApprovedReportController {
2 | approvedReport(params: string, body: Q): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/reports/domain/ports/primary/http/create-report.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ICreateReportController {
2 | create(body: Q, user): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/reports/domain/ports/primary/http/get-estimate.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IGetEstimateController {
2 | getEstimate(query: Q): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/reports/domain/ports/secondary/db/user.repository.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IReportRepositoryInterface {
2 | create(entity: D, options?: any): D;
3 | save(entity: D, options?: any): Promise;
4 | find(options?: any): Promise;
5 | findById(id: number): Promise;
6 | remove(entity: D, options?: any): Promise;
7 | getByQueryBuilder(entity: Partial, options?: any): Promise;
8 | }
9 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/approved-report/approved-report.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ApprovedReportController } from './approved-report.controller';
3 | import { ApprovedReportService } from '@reports/application/approved-report/approved-report.service';
4 |
5 | describe('ApprovedReportController', () => {
6 | let controller: ApprovedReportController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [ApprovedReportController],
11 | providers: [
12 | {
13 | provide: ApprovedReportService,
14 | useValue: {
15 | approved: async () => {
16 | return;
17 | },
18 | },
19 | },
20 | ],
21 | }).compile();
22 |
23 | controller = module.get(ApprovedReportController);
24 | });
25 |
26 | it('should be defined', () => {
27 | expect(controller).toBeDefined();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/approved-report/approved-report.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, HttpCode, HttpStatus, Param, Patch } from '@nestjs/common';
2 | import { ApprovedReportService } from '@reports/application/approved-report/approved-report.service';
3 | import { IApprovedReportController } from '@reports/domain/ports/primary/http/approved-report.controller.interface';
4 | import { ApprovedReportRequestDto } from './dto/approved.request.dto';
5 | import { IsAdmin } from '@shared/infrastructure/decorators/is-admin.decorator';
6 |
7 | @Controller('reports')
8 | export class ApprovedReportController implements IApprovedReportController {
9 | constructor(private approvedReportService: ApprovedReportService) {}
10 |
11 | @Patch('/:id')
12 | @IsAdmin()
13 | @HttpCode(HttpStatus.NO_CONTENT)
14 | async approvedReport(@Param('id') id: string, @Body() { isApproved }: ApprovedReportRequestDto): Promise {
15 | this.approvedReportService.changeApproved(parseInt(id), isApproved);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/approved-report/dto/approved.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsBoolean, IsNotEmpty } from 'class-validator';
2 |
3 | export class ApprovedReportRequestDto {
4 | @IsBoolean()
5 | @IsNotEmpty()
6 | isApproved: boolean;
7 | }
8 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/approved-report/request.http:
--------------------------------------------------------------------------------
1 |
2 | PATCH http://{{host}}/api/{{version}}/reports/1 HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
5 |
6 | {
7 | "isApproved": true
8 | }
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/create-report/create-report.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CreateReportController } from './create-report.controller';
3 | import { CreateReportService } from '@reports/application/create-report/create-report.service';
4 |
5 | describe('CreateReportController', () => {
6 | let controller: CreateReportController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [CreateReportController],
11 | providers: [
12 | {
13 | provide: CreateReportService,
14 | useValue: { create: async () => [{ id: 1, email: 'test@gmail.com', name: 'Test' }] },
15 | },
16 | ],
17 | }).compile();
18 |
19 | controller = module.get(CreateReportController);
20 | });
21 |
22 | it('should be defined', () => {
23 | expect(controller).toBeDefined();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/create-report/create-report.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
2 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
3 | import { ICreateReportController } from '@reports/domain/ports/primary/http/create-report.controller.interface';
4 | import { CreateReportRequestDto } from './dto/report.request.dto';
5 | import { CreatedReportRequestDto } from './dto/report.response.dto';
6 | import { CreateReportService } from '@reports/application/create-report/create-report.service';
7 | import { CurrentUser } from '@shared/infrastructure/decorators/current-user.decorator';
8 |
9 | @Controller('reports')
10 | export class CreateReportController
11 | implements ICreateReportController
12 | {
13 | constructor(private createReportService: CreateReportService) {}
14 |
15 | @Post('/')
16 | @HttpCode(HttpStatus.CREATED)
17 | @SerializeResponseDto(CreatedReportRequestDto)
18 | async create(@Body() body: CreateReportRequestDto, @CurrentUser() user): Promise {
19 | return this.createReportService.create(body, user);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/create-report/dto/report.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsLatitude, IsLongitude, IsNumber, IsString, Max, Min } from 'class-validator';
2 |
3 | export class CreateReportRequestDto {
4 | @IsString()
5 | make: string;
6 |
7 | @IsString()
8 | model?: string;
9 |
10 | @IsNumber()
11 | @Min(1930)
12 | @Max(2050)
13 | year: number;
14 |
15 | @IsNumber()
16 | @Min(0)
17 | @Max(1000000)
18 | mileage?: number;
19 |
20 | @IsLongitude()
21 | lng?: number;
22 |
23 | @IsLatitude()
24 | lat?: number;
25 |
26 | @IsNumber()
27 | @Min(0)
28 | @Max(1000000)
29 | price: number;
30 | }
31 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/create-report/dto/report.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { Expose } from 'class-transformer';
2 |
3 | export class CreatedReportRequestDto {
4 | @Expose()
5 | id: number;
6 |
7 | @Expose()
8 | make: string;
9 |
10 | @Expose()
11 | model?: string;
12 |
13 | @Expose()
14 | approved: boolean;
15 |
16 | @Expose()
17 | userId: number;
18 | }
19 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/create-report/request.http:
--------------------------------------------------------------------------------
1 |
2 | POST http://{{host}}/api/{{version}}/reports HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
5 |
6 | {
7 | "make": "Ferrary",
8 | "model": "WUIG78",
9 | "year": 2022,
10 | "mileage": 100,
11 | "lng": 45,
12 | "lat": 45,
13 | "price": 324
14 | }
15 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/get-estimate/dto/get-estimate.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { Transform } from 'class-transformer';
2 | import { IsString } from 'class-validator';
3 |
4 | export class GetEstimateRequestDto {
5 | @IsString()
6 | make?: string;
7 |
8 | @IsString()
9 | model?: string;
10 |
11 | @Transform(({ value }) => parseInt(value))
12 | year?: number;
13 |
14 | @Transform(({ value }) => parseInt(value))
15 | mileage?: number;
16 |
17 | @Transform(({ value }) => parseFloat(value))
18 | lng?: number;
19 |
20 | @Transform(({ value }) => parseFloat(value))
21 | lat?: number;
22 | }
23 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/get-estimate/dto/get-estimate.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { Expose } from 'class-transformer';
2 |
3 | export class GetEstimateResponseDto {
4 | @Expose()
5 | price?: number;
6 | }
7 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/get-estimate/get-estimate.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { GetEstimateController } from './get-estimate.controller';
3 | import { GetEstimateService } from '@reports/application/get-estimate/get-estimate.service';
4 |
5 | describe('GetEstimateController', () => {
6 | let controller: GetEstimateController;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | controllers: [GetEstimateController],
11 | providers: [
12 | {
13 | provide: GetEstimateService,
14 | useValue: {
15 | getEstimate: async () => ({
16 | price: 1000,
17 | }),
18 | },
19 | },
20 | ],
21 | }).compile();
22 |
23 | controller = module.get(GetEstimateController);
24 | });
25 |
26 | it('should be defined', () => {
27 | expect(controller).toBeDefined();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/get-estimate/get-estimate.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, HttpCode, HttpStatus, Patch, Query } from '@nestjs/common';
2 | import { GetEstimateRequestDto } from './dto/get-estimate.request.dto';
3 | import { IGetEstimateController } from '@reports/domain/ports/primary/http/get-estimate.controller.interface';
4 | import { GetEstimateService } from '@reports/application/get-estimate/get-estimate.service';
5 | import { GetEstimateResponseDto } from './dto/get-estimate.response.dto';
6 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
7 |
8 | @Controller('reports')
9 | export class GetEstimateController implements IGetEstimateController {
10 | constructor(private getEstimateService: GetEstimateService) {}
11 |
12 | @Patch('/')
13 | @HttpCode(HttpStatus.OK)
14 | @SerializeResponseDto(GetEstimateResponseDto)
15 | async getEstimate(@Query() query: GetEstimateRequestDto): Promise {
16 | return this.getEstimateService.getEstimate(query);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/primary/http/get-estimate/request.http:
--------------------------------------------------------------------------------
1 |
2 | PATCH http://{{host}}/api/{{version}}/reports?make=toyota&model=corolla&lng=12&lat=12&mileage=200&year=2023 HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
5 |
6 | ###other
7 | PATCH http://{{host}}/api/{{version}}/reports?make=Ferrary&model=WUIG78
8 | Content-Type: {{contentType}}
9 | Authorization: Bearer {{token}}
10 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/secondary/db/dao/report.dao.ts:
--------------------------------------------------------------------------------
1 | import { PrimaryGeneratedColumn, Column, Entity, ManyToOne } from 'typeorm';
2 | import { UserDao } from './user.dao';
3 |
4 | @Entity('Report')
5 | export class ReportDao {
6 | @PrimaryGeneratedColumn()
7 | id: number;
8 |
9 | @Column()
10 | price: number;
11 |
12 | @Column()
13 | make: string;
14 |
15 | @Column({
16 | nullable: true,
17 | })
18 | model?: string;
19 |
20 | @Column()
21 | year: number;
22 |
23 | @Column({
24 | nullable: true,
25 | })
26 | lng?: number;
27 |
28 | @Column({
29 | nullable: true,
30 | })
31 | lat?: number;
32 |
33 | @Column({
34 | nullable: true,
35 | })
36 | mileage?: number;
37 |
38 | @Column({ default: false })
39 | approved: boolean;
40 |
41 | @Column()
42 | userId: number;
43 |
44 | @ManyToOne(() => UserDao, (user) => user.reports)
45 | user: UserDao;
46 | }
47 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/secondary/db/dao/user.dao.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
2 | import { ReportDao } from './report.dao';
3 | import { Exclude } from 'class-transformer';
4 |
5 | @Entity('User')
6 | export class UserDao {
7 | @PrimaryGeneratedColumn()
8 | id: number;
9 |
10 | @Column()
11 | email: string;
12 |
13 | @Column()
14 | @Exclude()
15 | password: string;
16 |
17 | @Column({
18 | unique: false,
19 | nullable: true,
20 | })
21 | name?: string;
22 |
23 | @Column()
24 | isAdmin: boolean;
25 |
26 | @Column()
27 | createdAt: Date;
28 |
29 | @OneToMany(() => ReportDao, (report) => report.user)
30 | reports?: ReportDao[];
31 | }
32 |
--------------------------------------------------------------------------------
/src/reports/infrastructure/adapters/secondary/db/report.repository.ts:
--------------------------------------------------------------------------------
1 | import { FindManyOptions, RemoveOptions, Repository, SaveOptions } from 'typeorm';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Injectable } from '@nestjs/common';
4 | import { ReportDao } from './dao/report.dao';
5 | import { IReportRepositoryInterface } from '@reports/domain/ports/secondary/db/user.repository.interface';
6 |
7 | @Injectable()
8 | export class ReportRepository implements IReportRepositoryInterface {
9 | constructor(
10 | @InjectRepository(ReportDao)
11 | private repository: Repository,
12 | ) {}
13 |
14 | create(data: ReportDao): ReportDao {
15 | return this.repository.create(data);
16 | }
17 |
18 | save(entity: ReportDao, options?: SaveOptions): Promise {
19 | return this.repository.save(entity, options);
20 | }
21 |
22 | find(options?: FindManyOptions): Promise {
23 | return this.repository.find(options);
24 | }
25 |
26 | findById(id: number): Promise {
27 | return this.repository.findOneBy({ id });
28 | }
29 |
30 | remove(entity: ReportDao, options?: RemoveOptions): Promise {
31 | return this.repository.remove(entity, options);
32 | }
33 |
34 | // Estimado Promedio de precio de un auto
35 | getByQueryBuilder(query: Partial): Promise {
36 | return this.repository
37 | .createQueryBuilder()
38 | .select('AVG(price)', 'price')
39 | .where('make=:make', { make: query.make })
40 | .orWhere('model=:model', { model: query.model })
41 | .orWhere('year - :year', { year: query.year })
42 | .orWhere('lng - :lng BETWEEN -5 AND +5', { lng: query.lng })
43 | .orWhere('lat - :lat BETWEEN -3 AND +3', { lat: query.lat })
44 | .andWhere('approved IS TRUE')
45 | .orderBy('ABS(mileage - :mileage)', 'DESC')
46 | .setParameters({ mileage: query.mileage }) // esto es por el orderBy
47 | .limit(3)
48 | .getRawOne();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/reports/reports.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { ReportDao } from './infrastructure/adapters/secondary/db/dao/report.dao';
4 | import { CreateReportController } from './infrastructure/adapters/primary/http/create-report/create-report.controller';
5 | import { CreateReportService } from './application/create-report/create-report.service';
6 | import { ReportRepository } from './infrastructure/adapters/secondary/db/report.repository';
7 | import { ApprovedReportController } from './infrastructure/adapters/primary/http/approved-report/approved-report.controller';
8 | import { ApprovedReportService } from './application/approved-report/approved-report.service';
9 | import { GetEstimateController } from './infrastructure/adapters/primary/http/get-estimate/get-estimate.controller';
10 | import { GetEstimateService } from './application/get-estimate/get-estimate.service';
11 |
12 | @Module({
13 | imports: [TypeOrmModule.forFeature([ReportDao])],
14 | providers: [CreateReportService, ReportRepository, ApprovedReportService, GetEstimateService],
15 | controllers: [CreateReportController, ApprovedReportController, GetEstimateController],
16 | })
17 | export class ReportsModule {}
18 |
--------------------------------------------------------------------------------
/src/setup-app.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication, ValidationPipe } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import * as path from 'path';
4 |
5 | export const setupApp = (app: INestApplication) => {
6 | const packageJson = require(path.resolve('package.json'));
7 | process.env.API_VERSION = packageJson.version;
8 |
9 | // app.set('trust proxy', true); //esto no me acuerdo para que era
10 |
11 | // esto es para validar los DTO
12 | app.useGlobalPipes(
13 | new ValidationPipe({
14 | transform: true, // Automatically transform payloads to DTO instances
15 | whitelist: true, // esto sirve para evitar que se meta churre en el endpoind
16 | forbidNonWhitelisted: true, // Throw an error if payload contains non-whitelisted properties
17 | }),
18 | );
19 | app.setGlobalPrefix('api/v1');
20 |
21 | const configService = app.get(ConfigService);
22 | app.enableCors({
23 | origin: configService.getOrThrow('CORS_ALLOWED_ORIGIN'),
24 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
25 | preflightContinue: false,
26 | optionsSuccessStatus: 204,
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/src/shared/domain/errors/value-required-error.ts:
--------------------------------------------------------------------------------
1 | export class ValueRequiredError extends Error {
2 | constructor(private value: string) {
3 | super(`Value is required: ${value}`);
4 | this.name = 'ValueRequiredError';
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/shared/domain/value-objects/email.value.object.ts:
--------------------------------------------------------------------------------
1 | import { EMAIL_PATTERN } from '@users/constants';
2 | import { ValueObjectBase } from './value-object-base.abstract';
3 | import { ValueRequiredError } from '../errors/value-required-error';
4 |
5 | export class EmailValueObject extends ValueObjectBase {
6 | constructor(value: string) {
7 | super(value);
8 | this.setPattern(EMAIL_PATTERN);
9 | if (!value) {
10 | throw new ValueRequiredError('email');
11 | }
12 |
13 | if (!this.isValid(value)) {
14 | throw new ValueRequiredError('email');
15 | }
16 | }
17 |
18 | get getDomain(): string {
19 | return this.value.split('@')[1];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/shared/domain/value-objects/password.value.object.ts:
--------------------------------------------------------------------------------
1 | import { ValueRequiredError } from '../errors/value-required-error';
2 | import { ValueObjectBase } from './value-object-base.abstract';
3 |
4 | export class PasswordValueObject extends ValueObjectBase {
5 | constructor(value: string) {
6 | super(value);
7 | if (!value) {
8 | throw new ValueRequiredError('password');
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/shared/domain/value-objects/value-object-base.abstract.ts:
--------------------------------------------------------------------------------
1 | import { shallowEqual } from 'shallow-equal-object';
2 |
3 | export abstract class ValueObjectBase {
4 | protected readonly value: T;
5 | private PATTERN: RegExp;
6 |
7 | constructor(value: T) {
8 | this.value = Object.freeze(value);
9 | }
10 |
11 | public equals(vo?: ValueObjectBase): boolean {
12 | if (vo === null || vo === undefined) {
13 | return false;
14 | }
15 | if (vo.value === undefined) {
16 | return false;
17 | }
18 | return shallowEqual(this.value, vo.value);
19 | }
20 |
21 | public get getValue(): T {
22 | return this.value;
23 | }
24 |
25 | isValid(value): boolean {
26 | return this.PATTERN.test(value);
27 | }
28 |
29 | setPattern(newPattern: RegExp): void {
30 | this.PATTERN = newPattern;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/shared/infrastructure/decorators/current-user.decorator.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common';
2 |
3 | export const CurrentUser = createParamDecorator((data: never, context: ExecutionContext) => {
4 | const request = context.switchToHttp().getRequest();
5 | // Por defecto la estrategia de jwt guarda el user del jwt en request.user
6 | return request.user;
7 | });
8 |
--------------------------------------------------------------------------------
/src/shared/infrastructure/decorators/is-admin.decorator.ts:
--------------------------------------------------------------------------------
1 | import { UseGuards } from '@nestjs/common';
2 | import { IsAdminGuard } from '../../../auth/infrastructure/guards/is-admin.guard';
3 |
4 | export function IsAdmin(): MethodDecorator & ClassDecorator {
5 | return UseGuards(new IsAdminGuard());
6 | }
7 |
--------------------------------------------------------------------------------
/src/shared/infrastructure/decorators/public.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 |
3 | export const IS_PUBLIC_KEY = 'isPublic';
4 | export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
5 |
--------------------------------------------------------------------------------
/src/shared/infrastructure/decorators/serialize.decorator.ts:
--------------------------------------------------------------------------------
1 | import { UseInterceptors } from '@nestjs/common';
2 | import { ResponseBaseDto } from '../response-base.dto.abstract';
3 | import { SerializeInterceptor } from '../interceptors/serialize.interceptor';
4 |
5 | export function SerializeResponseDto(dto: ResponseBaseDto): MethodDecorator & ClassDecorator {
6 | return UseInterceptors(new SerializeInterceptor(dto));
7 | }
8 |
--------------------------------------------------------------------------------
/src/shared/infrastructure/interceptors/serialize.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2 | import { ClassConstructor, plainToInstance } from 'class-transformer';
3 | import { Observable, map } from 'rxjs';
4 | import { ResponseBaseDto } from '../response-base.dto.abstract';
5 |
6 | @Injectable()
7 | export class SerializeInterceptor implements NestInterceptor {
8 | constructor(private dto: ResponseBaseDto) {}
9 | intercept(context: ExecutionContext, next: CallHandler): Observable {
10 | return next.handle().pipe(
11 | map((data: any) => {
12 | return plainToInstance(this.dto as ClassConstructor, data, {
13 | excludeExtraneousValues: true,
14 | });
15 | }),
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/shared/infrastructure/request-base.dto.abstract.ts:
--------------------------------------------------------------------------------
1 | export abstract class RequestBaseDto {}
2 |
--------------------------------------------------------------------------------
/src/shared/infrastructure/response-base.dto.abstract.ts:
--------------------------------------------------------------------------------
1 | export abstract class ResponseBaseDto {}
2 |
--------------------------------------------------------------------------------
/src/users/application/find-user-by-id/find-user-by-id.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { FindUserByIdService } from './find-user-by-id.service';
3 | import { getRepositoryToken } from '@nestjs/typeorm';
4 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
5 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository';
6 |
7 | describe('FindUserByIdService', () => {
8 | let service: FindUserByIdService;
9 | const mockRepository = {
10 | findById: jest.fn().mockImplementation((dao: UserDao) => {
11 | return Promise.resolve({
12 | id: Math.ceil(Math.random() * 10),
13 | ...dao,
14 | });
15 | }),
16 | };
17 |
18 | beforeEach(async () => {
19 | const moduleRef = await Test.createTestingModule({
20 | imports: [], // Add
21 | controllers: [], // Add
22 | providers: [
23 | FindUserByIdService,
24 | UserRepository,
25 | {
26 | provide: getRepositoryToken(UserDao),
27 | useValue: mockRepository.findById(),
28 | },
29 | ], // Add
30 | }).compile();
31 |
32 | service = moduleRef.get(FindUserByIdService);
33 | });
34 |
35 | it('should be defined', () => {
36 | expect(service).toBeDefined();
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/users/application/find-user-by-id/find-user-by-id.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
3 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository';
4 |
5 | @Injectable()
6 | export class FindUserByIdService {
7 | constructor(private userRepository: UserRepository) {}
8 |
9 | async find(id: number): Promise {
10 | if (!id) {
11 | return null;
12 | }
13 |
14 | const user: UserDao = await this.userRepository.findById(id);
15 |
16 | if (!user) {
17 | throw new NotFoundException('User not found!');
18 | }
19 |
20 | return user;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/users/application/find-users/find-users.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { getRepositoryToken } from '@nestjs/typeorm';
3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
4 | import { FindUsersService } from './find-users.service';
5 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository';
6 |
7 | describe('FindUsersService', () => {
8 | let service: FindUsersService;
9 | const mockRepository = {
10 | find: jest.fn().mockImplementation((dao: UserDao) => {
11 | return Promise.resolve([
12 | {
13 | id: Math.ceil(Math.random() * 10),
14 | ...dao,
15 | },
16 | {
17 | id: Math.ceil(Math.random() * 10),
18 | ...dao,
19 | },
20 | ]);
21 | }),
22 | };
23 |
24 | beforeEach(async () => {
25 | const moduleRef = await Test.createTestingModule({
26 | imports: [], // Add
27 | controllers: [], // Add
28 | providers: [
29 | FindUsersService,
30 | UserRepository,
31 | {
32 | provide: getRepositoryToken(UserDao),
33 | useValue: mockRepository,
34 | },
35 | ], // Add
36 | }).compile();
37 |
38 | service = moduleRef.get(FindUsersService);
39 | });
40 |
41 | it('should be defined', () => {
42 | expect(service).toBeDefined();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/users/application/find-users/find-users.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
3 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository';
4 |
5 | @Injectable()
6 | export class FindUsersService {
7 | constructor(private userRepository: UserRepository) {}
8 |
9 | async find(): Promise {
10 | const users: UserDao[] = await this.userRepository.find({ relations: ['reports'] });
11 |
12 | return users;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/users/application/remove-user/remove-user.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { RemoveUserService } from './remove-user.service';
3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
4 | import { getRepositoryToken } from '@nestjs/typeorm';
5 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository';
6 |
7 | describe('RemoveUserService', () => {
8 | let service: RemoveUserService;
9 | const mockRepository = {
10 | findById: jest.fn().mockImplementation((dao: UserDao) => {
11 | return Promise.resolve({
12 | id: Math.ceil(Math.random() * 10),
13 | ...dao,
14 | });
15 | }),
16 | remove: jest.fn().mockImplementation((dao: UserDao) => {
17 | return Promise.resolve({
18 | id: Math.ceil(Math.random() * 10),
19 | ...dao,
20 | });
21 | }),
22 | };
23 |
24 | beforeEach(async () => {
25 | const moduleRef = await Test.createTestingModule({
26 | imports: [], // Add
27 | controllers: [], // Add
28 | providers: [
29 | RemoveUserService,
30 | UserRepository,
31 | {
32 | provide: getRepositoryToken(UserDao),
33 | useValue: mockRepository,
34 | },
35 | ], // Add
36 | }).compile();
37 |
38 | service = moduleRef.get(RemoveUserService);
39 | });
40 |
41 | it('should be defined', () => {
42 | expect(service).toBeDefined();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/users/application/remove-user/remove-user.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
3 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository';
4 |
5 | @Injectable()
6 | export class RemoveUserService {
7 | constructor(private userRepository: UserRepository) {}
8 |
9 | async remove(id: number): Promise {
10 | const user: UserDao = await this.userRepository.findById(id);
11 |
12 | if (!user) {
13 | throw new NotFoundException('User not found!');
14 | }
15 |
16 | return this.userRepository.remove(user);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/users/application/update-user/update-user.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { UpdateUserService } from './update-user.service';
3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
4 | import { getRepositoryToken } from '@nestjs/typeorm';
5 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository';
6 |
7 | describe('UpdateUserService', () => {
8 | let service: UpdateUserService;
9 | const mockRepository = {
10 | findById: jest.fn().mockImplementation((dao: UserDao) => {
11 | return Promise.resolve({
12 | id: Math.ceil(Math.random() * 10),
13 | ...dao,
14 | });
15 | }),
16 | save: jest.fn().mockImplementation((dao: UserDao) => {
17 | return Promise.resolve({
18 | id: Math.ceil(Math.random() * 10),
19 | ...dao,
20 | });
21 | }),
22 | };
23 |
24 | beforeEach(async () => {
25 | const moduleRef = await Test.createTestingModule({
26 | imports: [], // Add
27 | controllers: [], // Add
28 | providers: [
29 | UpdateUserService,
30 | UserRepository,
31 | {
32 | provide: getRepositoryToken(UserDao),
33 | useValue: mockRepository,
34 | },
35 | ], // Add
36 | }).compile();
37 |
38 | service = moduleRef.get(UpdateUserService);
39 | });
40 |
41 | it('should be defined', () => {
42 | expect(service).toBeDefined();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/users/application/update-user/update-user.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
3 | import { UserRepository } from '@users/infrastructure/adapters/secondary/db/user.repository';
4 |
5 | @Injectable()
6 | export class UpdateUserService {
7 | constructor(private userRepository: UserRepository) {}
8 |
9 | async update(id: number, attrs: Partial) {
10 | const user: UserDao = await this.userRepository.findById(id);
11 |
12 | if (!user) {
13 | throw new NotFoundException('User not found!');
14 | }
15 |
16 | Object.assign(user, attrs); // le asignamos lo que esta en attrs a lo que esta en user
17 |
18 | return this.userRepository.save(user);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/users/constants.ts:
--------------------------------------------------------------------------------
1 | export const EMAIL_PATTERN = /^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/;
2 |
--------------------------------------------------------------------------------
/src/users/domain/entity/user.ts:
--------------------------------------------------------------------------------
1 | import { EmailValueObject } from '@shared/domain/value-objects/email.value.object';
2 |
3 | export class User {
4 | constructor(private email: EmailValueObject, private name: string) {}
5 |
6 | async toJSON() {
7 | return {
8 | email: this.email.getValue,
9 | name: this.name,
10 | };
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/users/domain/ports/primary/api/find-user-by-id.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IFindUserByIdController {
2 | findUserById(param: P): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/users/domain/ports/primary/api/find-users.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IFindUsersController {
2 | find(query: Q): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/users/domain/ports/primary/api/remove-user.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IRemoveUserController {
2 | remove(param: P): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/users/domain/ports/primary/api/update.controller.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IUpdateUserController {
2 | update(id: string, body: B): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/src/users/domain/ports/secondary/db/user.repository.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IUserRepositoryInterface {
2 | save(entity: D, options?: any): Promise;
3 | find(options?: any): Promise;
4 | findById(id: number): Promise;
5 | remove(entity: D, options?: any): Promise;
6 | }
7 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-user-by-id/dto/user.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract';
2 | import { Expose } from 'class-transformer';
3 |
4 | export class UserResponseDto extends ResponseBaseDto {
5 | @Expose()
6 | id: number;
7 |
8 | @Expose()
9 | email: string;
10 |
11 | @Expose()
12 | name?: string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-user-by-id/find-user-by-id.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { FindUserByIdService } from '@users/application/find-user-by-id/find-user-by-id.service';
3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
4 | import { FindUserByIdController } from './find-user-by-id.controller';
5 |
6 | describe('UsersController', () => {
7 | let controller: FindUserByIdController;
8 |
9 | beforeEach(async () => {
10 | const module: TestingModule = await Test.createTestingModule({
11 | controllers: [FindUserByIdController],
12 | providers: [
13 | {
14 | provide: FindUserByIdService,
15 | useValue: { find: async () => ({ id: 1, email: 'test@gmail.com', name: 'Test' } as UserDao) },
16 | },
17 | ],
18 | }).compile();
19 |
20 | controller = module.get(FindUserByIdController);
21 | });
22 |
23 | it('should be defined', () => {
24 | expect(controller).toBeDefined();
25 | });
26 |
27 | it('should find the user by id', async () => {
28 | const user = await controller.findUserById('1');
29 | expect(user.id).toBe(1);
30 | expect(user.email).toBe('test@gmail.com');
31 | expect(user.name).toBe('Test');
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-user-by-id/find-user-by-id.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
2 | import { UserResponseDto } from './dto/user.response.dto';
3 | import { IFindUserByIdController } from '@users/domain/ports/primary/api/find-user-by-id.controller.interface';
4 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
5 | import { FindUserByIdService } from '@users/application/find-user-by-id/find-user-by-id.service';
6 |
7 | @Controller('users')
8 | export class FindUserByIdController implements IFindUserByIdController {
9 | constructor(private findUserByIdService: FindUserByIdService) {}
10 |
11 | @Get('/:id')
12 | @HttpCode(HttpStatus.OK)
13 | @SerializeResponseDto(UserResponseDto)
14 | async findUserById(@Param('id') id: string): Promise {
15 | return this.findUserByIdService.find(parseInt(id));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-user-by-id/request.http:
--------------------------------------------------------------------------------
1 |
2 | GET http://{{host}}/api/{{version}}/users/1 HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
5 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-users/dto/product.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { Expose } from 'class-transformer';
2 |
3 | export class Product {
4 | @Expose()
5 | id: number;
6 |
7 | @Expose()
8 | price: number;
9 | }
10 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-users/dto/user.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract';
2 | import { Expose, Type } from 'class-transformer';
3 | import { Product } from './product.response.dto';
4 |
5 | export class UserResponseDto extends ResponseBaseDto {
6 | @Expose()
7 | id: number;
8 |
9 | @Expose()
10 | email: string;
11 |
12 | @Expose()
13 | name?: string;
14 |
15 | @Expose()
16 | createdAt: Date;
17 |
18 | @Expose()
19 | updatedAt?: Date;
20 |
21 | @Expose()
22 | @Type(() => Product)
23 | reports?: Product[];
24 | }
25 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-users/find-users.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { FindUsersController } from './find-users.controller';
3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
4 | import { FindUsersService } from '@users/application/find-users/find-users.service';
5 |
6 | describe('FindUsersController', () => {
7 | let controller: FindUsersController;
8 |
9 | beforeEach(async () => {
10 | const module: TestingModule = await Test.createTestingModule({
11 | controllers: [FindUsersController],
12 | providers: [
13 | {
14 | provide: FindUsersService,
15 | useValue: { find: async () => [{ id: 1, email: 'test@gmail.com', name: 'Test' }] as UserDao[] },
16 | },
17 | ],
18 | }).compile();
19 |
20 | controller = module.get(FindUsersController);
21 | });
22 |
23 | it('should be defined', () => {
24 | expect(controller).toBeDefined();
25 | });
26 |
27 | it('should find all users', async () => {
28 | const users = await controller.find();
29 | expect(users.length).toBe(1);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-users/find-users.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, HttpCode, HttpStatus } from '@nestjs/common';
2 | import { UserResponseDto } from './dto/user.response.dto';
3 | import { IFindUsersController } from '@users/domain/ports/primary/api/find-users.controller.interface';
4 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
5 | import { FindUsersService } from '@users/application/find-users/find-users.service';
6 |
7 | @Controller('users')
8 | export class FindUsersController implements IFindUsersController {
9 | constructor(private findUsersService: FindUsersService) {}
10 |
11 | @Get('/')
12 | @HttpCode(HttpStatus.OK)
13 | @SerializeResponseDto(UserResponseDto)
14 | async find(): Promise {
15 | return this.findUsersService.find();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/find-users/request.http:
--------------------------------------------------------------------------------
1 |
2 | GET http://{{host}}/api/{{version}}/users HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
5 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/remove-user/dto/user.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { Expose } from 'class-transformer';
2 |
3 | export class UserResponseDto {
4 | @Expose()
5 | email: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/remove-user/remove-user.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { RemoveUserController } from './remove-user.controller';
3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
4 | import { RemoveUserService } from '@users/application/remove-user/remove-user.service';
5 |
6 | describe('RemoveUserController', () => {
7 | let controller: RemoveUserController;
8 |
9 | beforeEach(async () => {
10 | const module: TestingModule = await Test.createTestingModule({
11 | controllers: [RemoveUserController],
12 | providers: [
13 | {
14 | provide: RemoveUserService,
15 | useValue: { remove: async () => ({ id: 1, email: 'test@gmail.com', name: 'Test' } as UserDao) },
16 | },
17 | ],
18 | }).compile();
19 |
20 | controller = module.get(RemoveUserController);
21 | });
22 |
23 | it('should be defined', () => {
24 | expect(controller).toBeDefined();
25 | });
26 |
27 | it('should remove an user', async () => {
28 | const user = await controller.remove('1');
29 | expect(user.email).toBe('test@gmail.com');
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/remove-user/remove-user.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Delete, HttpCode, HttpStatus, Param } from '@nestjs/common';
2 | import { UserResponseDto } from './dto/user.response.dto';
3 | import { IRemoveUserController } from '@users/domain/ports/primary/api/remove-user.controller.interface';
4 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
5 | import { RemoveUserService } from '@users/application/remove-user/remove-user.service';
6 |
7 | @Controller('users')
8 | export class RemoveUserController implements IRemoveUserController {
9 | constructor(private removeUserService: RemoveUserService) {}
10 |
11 | @Delete('/:id')
12 | @HttpCode(HttpStatus.OK)
13 | @SerializeResponseDto(UserResponseDto)
14 | async remove(@Param('id') id: string): Promise {
15 | return this.removeUserService.remove(parseInt(id));
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/remove-user/request.http:
--------------------------------------------------------------------------------
1 |
2 | DELETE http://{{host}}/api/{{version}}/users/2 HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
5 |
6 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/update-user/dto/user.request.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsEmail, IsOptional, IsString } from 'class-validator';
2 |
3 | export class UpdateRequestDto {
4 | @IsOptional()
5 | @IsEmail()
6 | email?: string;
7 |
8 | @IsOptional()
9 | @IsString()
10 | password?: string;
11 |
12 | @IsOptional()
13 | @IsString()
14 | name?: string;
15 | }
16 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/update-user/dto/user.response.dto.ts:
--------------------------------------------------------------------------------
1 | import { ResponseBaseDto } from '@shared/infrastructure/response-base.dto.abstract';
2 | import { Expose } from 'class-transformer';
3 | export class UserResponseDto extends ResponseBaseDto {
4 | @Expose()
5 | email: string;
6 |
7 | @Expose()
8 | name?: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/update-user/request.http:
--------------------------------------------------------------------------------
1 |
2 | PATCH http://{{host}}/api/{{version}}/users/2 HTTP/1.1
3 | Content-Type: {{contentType}}
4 | Authorization: Bearer {{token}}
5 |
6 | {
7 | "email": "yasniel22222@gmail.com",
8 | "password": "yasniell",
9 | "name": "Yasniel2"
10 | }
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/update-user/update-user.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UpdateUserController } from './update-user.controller';
3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
4 | import { UpdateUserService } from '@users/application/update-user/update-user.service';
5 |
6 | describe('UsersController', () => {
7 | let controller: UpdateUserController;
8 |
9 | beforeEach(async () => {
10 | const module: TestingModule = await Test.createTestingModule({
11 | controllers: [UpdateUserController],
12 | providers: [
13 | {
14 | provide: UpdateUserService,
15 | useValue: { update: async () => ({ id: 1, email: 'test@gmail.com', name: 'Test' } as UserDao) },
16 | },
17 | ],
18 | }).compile();
19 |
20 | controller = module.get(UpdateUserController);
21 | });
22 |
23 | it('should be defined', () => {
24 | expect(controller).toBeDefined();
25 | });
26 |
27 | it('should remove an user', async () => {
28 | const user = await controller.update('1', { email: 'test@gmail.com', name: 'Test' });
29 | expect(user.email).toBe('test@gmail.com');
30 | expect(user.name).toBe('Test');
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/primary/http/update-user/update-user.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, HttpCode, HttpStatus, Param, Patch } from '@nestjs/common';
2 | import { UpdateRequestDto } from './dto/user.request.dto';
3 | import { UserResponseDto } from './dto/user.response.dto';
4 | import { IUpdateUserController } from '@users/domain/ports/primary/api/update.controller.interface';
5 | import { SerializeResponseDto } from '@shared/infrastructure/decorators/serialize.decorator';
6 | import { UpdateUserService } from '@users/application/update-user/update-user.service';
7 |
8 | @Controller('users')
9 | export class UpdateUserController implements IUpdateUserController {
10 | constructor(private updateUserService: UpdateUserService) {}
11 |
12 | @Patch('/:id')
13 | @HttpCode(HttpStatus.OK)
14 | @SerializeResponseDto(UserResponseDto)
15 | async update(@Param('id') id: string, @Body() body: UpdateRequestDto): Promise {
16 | return this.updateUserService.update(parseInt(id), body);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/secondary/db/dao/report.dao.ts:
--------------------------------------------------------------------------------
1 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
2 | import { PrimaryGeneratedColumn, Column, Entity, ManyToOne } from 'typeorm';
3 |
4 | @Entity('Report')
5 | export class ReportDao {
6 | @PrimaryGeneratedColumn()
7 | id: number;
8 |
9 | @Column()
10 | make: string;
11 |
12 | @Column()
13 | price: number;
14 |
15 | @Column()
16 | year: number;
17 |
18 | @ManyToOne(() => UserDao, (user) => user.reports)
19 | user: UserDao;
20 | }
21 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/secondary/db/dao/user.dao.ts:
--------------------------------------------------------------------------------
1 | import { Exclude, Type } from 'class-transformer';
2 | import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
3 | import { ReportDao } from './report.dao';
4 |
5 | @Entity('User')
6 | export class UserDao {
7 | @PrimaryGeneratedColumn()
8 | id: number;
9 |
10 | @Column()
11 | email: string;
12 |
13 | @Column()
14 | @Exclude()
15 | password: string;
16 |
17 | @Column({
18 | unique: false,
19 | nullable: true,
20 | })
21 | name?: string;
22 |
23 | @Column({
24 | nullable: true,
25 | length: 500,
26 | })
27 | @Exclude()
28 | refreshToken: string;
29 |
30 | @Column()
31 | isAdmin: boolean;
32 |
33 | @CreateDateColumn()
34 | createdAt: Date;
35 |
36 | @UpdateDateColumn()
37 | updatedAt?: Date;
38 |
39 | @OneToMany((type) => ReportDao, (report) => report.user)
40 | @Type()
41 | reports?: ReportDao[];
42 |
43 | // @AfterInsert()
44 | // logInstert() {
45 | // // eslint-disable-next-line no-console
46 | // console.log('Insert ID: ', this.id);
47 | // }
48 |
49 | // @AfterUpdate()
50 | // logUpdate() {
51 | // // eslint-disable-next-line no-console
52 | // console.log('Update ID: ', this.id);
53 | // }
54 |
55 | // @AfterRemove()
56 | // logRemove() {
57 | // // eslint-disable-next-line no-console
58 | // console.log('Remove ID: ', this.id);
59 | // }
60 | }
61 |
--------------------------------------------------------------------------------
/src/users/infrastructure/adapters/secondary/db/user.repository.ts:
--------------------------------------------------------------------------------
1 | import { IUserRepositoryInterface } from '@users/domain/ports/secondary/db/user.repository.interface';
2 | import { FindManyOptions, RemoveOptions, Repository, SaveOptions } from 'typeorm';
3 | import { UserDao } from './dao/user.dao';
4 | import { InjectRepository } from '@nestjs/typeorm';
5 | import { Injectable } from '@nestjs/common';
6 |
7 | @Injectable()
8 | export class UserRepository implements IUserRepositoryInterface {
9 | constructor(
10 | @InjectRepository(UserDao)
11 | private repository: Repository,
12 | ) {}
13 |
14 | save(entity: UserDao, options?: SaveOptions): Promise {
15 | return this.repository.save(entity, options);
16 | }
17 |
18 | find(options?: FindManyOptions): Promise {
19 | return this.repository.find(options);
20 | }
21 |
22 | findById(id: number): Promise {
23 | return this.repository.findOneBy({ id });
24 | }
25 |
26 | remove(entity: UserDao, options?: RemoveOptions): Promise {
27 | return this.repository.remove(entity, options);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { UserDao } from '@users/infrastructure/adapters/secondary/db/dao/user.dao';
4 | import { FindUserByIdController } from './infrastructure/adapters/primary/http/find-user-by-id/find-user-by-id.controller';
5 | import { FindUsersController } from './infrastructure/adapters/primary/http/find-users/find-users.controller';
6 | import { RemoveUserController } from './infrastructure/adapters/primary/http/remove-user/remove-user.controller';
7 | import { UpdateUserController } from './infrastructure/adapters/primary/http/update-user/update-user.controller';
8 | import { FindUserByIdService } from './application/find-user-by-id/find-user-by-id.service';
9 | import { UserRepository } from './infrastructure/adapters/secondary/db/user.repository';
10 | import { FindUsersService } from './application/find-users/find-users.service';
11 | import { RemoveUserService } from './application/remove-user/remove-user.service';
12 | import { UpdateUserService } from './application/update-user/update-user.service';
13 |
14 | @Module({
15 | imports: [TypeOrmModule.forFeature([UserDao])],
16 | exports: [],
17 | providers: [FindUserByIdService, FindUsersService, RemoveUserService, UpdateUserService, UserRepository],
18 | controllers: [FindUserByIdController, FindUsersController, RemoveUserController, UpdateUserController],
19 | })
20 | export class UsersModule {}
21 |
--------------------------------------------------------------------------------
/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 '@app/app.module';
5 | import { setupApp } from '@src/setup-app';
6 |
7 | describe('AppController (e2e)', () => {
8 | let app: INestApplication;
9 |
10 | beforeEach(async () => {
11 | const moduleFixture: TestingModule = await Test.createTestingModule({
12 | imports: [AppModule],
13 | }).compile();
14 |
15 | app = moduleFixture.createNestApplication();
16 | setupApp(app);
17 | await app.init();
18 | });
19 |
20 | it('/ (GET)', () => {
21 | return request(app.getHttpServer()).get('/api/v1').expect(200).expect({ version: '0.0.1', env: 'qa' });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/auth/signin.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { setupApp } from '@src/setup-app';
2 | import { AppModule } from '@app/app.module';
3 | import { INestApplication } from '@nestjs/common';
4 | import { Test, TestingModule } from '@nestjs/testing';
5 | import * as request from 'supertest';
6 | import { ConfigService } from '@nestjs/config';
7 | describe('Auth signin (e2e)', () => {
8 | let app: INestApplication;
9 | const email = 'test@gmail.com';
10 |
11 | beforeEach(async () => {
12 | const moduleFixture: TestingModule = await Test.createTestingModule({
13 | imports: [AppModule],
14 | providers: [ConfigService],
15 | }).compile();
16 |
17 | app = moduleFixture.createNestApplication();
18 | setupApp(app);
19 | await app.init();
20 | });
21 |
22 | it('handles a signin request / (POST)', async () => {
23 | await request(app.getHttpServer())
24 | .post('/api/v1/auth/signup')
25 | .send({ email, password: 'test' })
26 | .expect(201)
27 | .then((res) => {
28 | const { id, email: emailNewUser } = res.body;
29 | expect(id).toBeDefined();
30 | expect(emailNewUser).toEqual(email);
31 | });
32 |
33 | return request(app.getHttpServer())
34 | .post('/api/v1/auth/signin')
35 | .send({ email, password: 'test' })
36 | .expect(200)
37 | .then((res) => {
38 | const { token } = res.body;
39 | expect(token).toBeDefined();
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/auth/signout.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { setupApp } from '@src/setup-app';
2 | import { AppModule } from '@app/app.module';
3 | import { INestApplication } from '@nestjs/common';
4 | import { Test, TestingModule } from '@nestjs/testing';
5 | import * as request from 'supertest';
6 |
7 | describe('Auth signout (e2e)', () => {
8 | let app: INestApplication;
9 | const email = 'test@gmail.com';
10 |
11 | beforeEach(async () => {
12 | const moduleFixture: TestingModule = await Test.createTestingModule({
13 | imports: [AppModule],
14 | }).compile();
15 |
16 | app = moduleFixture.createNestApplication();
17 | setupApp(app);
18 | await app.init();
19 | });
20 |
21 | it('handles a signout request / (POST)', () => {
22 | return request(app.getHttpServer())
23 | .post('/api/v1/auth/signout')
24 | .send({ email, password: 'test' })
25 | .set(
26 | 'Authorization',
27 | 'Bearer ' +
28 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJpYXQiOjE2OTEzNjk2ODl9.tnCeh4KgF68oJW5xjXL9EErdRpcAi0XSnFYOryEAHRs',
29 | )
30 | .expect(200)
31 | .then((res) => {
32 | const { ok } = res.body;
33 | expect(ok).toBeDefined();
34 | expect(ok).toBeTruthy();
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/test/auth/signup.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { setupApp } from '@src/setup-app';
2 | import { AppModule } from '@app/app.module';
3 | import { INestApplication } from '@nestjs/common';
4 | import { Test, TestingModule } from '@nestjs/testing';
5 | import * as request from 'supertest';
6 |
7 | describe('Auth signout (e2e)', () => {
8 | let app: INestApplication;
9 |
10 | beforeEach(async () => {
11 | const moduleFixture: TestingModule = await Test.createTestingModule({
12 | imports: [AppModule],
13 | }).compile();
14 |
15 | app = moduleFixture.createNestApplication();
16 | setupApp(app);
17 | await app.init();
18 | });
19 |
20 | it('handles a signup request / (POST)', () => {
21 | const email = 'test@gmail.com';
22 | return request(app.getHttpServer())
23 | .post('/api/v1/auth/signup')
24 | .send({ email, password: 'test' })
25 | .expect(201)
26 | .then((res) => {
27 | const { id, email: emailNewUser } = res.body;
28 | expect(id).toBeDefined();
29 | expect(emailNewUser).toEqual(email);
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/test/auth/whoami.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { setupApp } from '@src/setup-app';
2 | import { AppModule } from '@app/app.module';
3 | import { INestApplication } from '@nestjs/common';
4 | import { Test, TestingModule } from '@nestjs/testing';
5 | import * as request from 'supertest';
6 | describe('Auth whoami (e2e)', () => {
7 | let app: INestApplication;
8 | const email = 'test@gmail.com';
9 |
10 | beforeEach(async () => {
11 | const moduleFixture: TestingModule = await Test.createTestingModule({
12 | imports: [AppModule],
13 | }).compile();
14 |
15 | app = moduleFixture.createNestApplication();
16 | setupApp(app);
17 | await app.init();
18 | });
19 |
20 | it('handles a whoami request / (POST)', async () => {
21 | await request(app.getHttpServer())
22 | .post('/api/v1/auth/signup')
23 | .send({ email, password: 'test' })
24 | .expect(201)
25 | .then((res) => {
26 | const { id, email: emailNewUser } = res.body;
27 | expect(id).toBeDefined();
28 | expect(emailNewUser).toEqual(email);
29 | });
30 |
31 | return request(app.getHttpServer())
32 | .post('/api/v1/auth/whoami')
33 | .send({ email, password: 'test' })
34 | .set(
35 | 'Authorization',
36 | 'Bearer ' +
37 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJpYXQiOjE2OTEzNjk2ODl9.tnCeh4KgF68oJW5xjXL9EErdRpcAi0XSnFYOryEAHRs',
38 | )
39 | .expect(200)
40 | .then((res) => {
41 | const { email: emailNewUser } = res.body;
42 | expect(emailNewUser).toBeDefined();
43 | expect(email).toEqual(emailNewUser);
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/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 | "moduleNameMapper": {
10 | "^@src(.*)$": "/../src$1",
11 | "^@app(.*)$": "/../src/app$1",
12 | "^@config(.*)$": "/../src/config$1",
13 | "^@users(.*)$": "/../src/users$1",
14 | "^@auth(.*)$": "/../src/auth$1",
15 | "^@reports(.*)$": "/../src/reports$1",
16 | "^@utils(.*)$": "/../src/utils$1",
17 | "^@shared(.*)$": "/../src/shared$1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/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 | "paths": {
21 | "@src/*": ["src/*"],
22 | "@app/*": ["src/app/*"],
23 | "@config/*": ["src/config/*"],
24 | "@users/*": ["src/users/*"],
25 | "@auth/*": ["src/auth/*"],
26 | "@reports/*": ["src/reports/*"],
27 | "@utils/*": ["src/utils/*"],
28 | "@shared/*": ["src/shared/*"],
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------