Agregar campo username a User entity y DTO
- Columna username (unique, nullable) en auth.users - Campo username en CreateUserDto y UpdateUserDto Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
33
src/modules/favorites/dto/create-favorite.dto.ts
Normal file
33
src/modules/favorites/dto/create-favorite.dto.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEnum, IsUUID, IsString, IsOptional, IsObject } from 'class-validator';
|
||||
import { FavoriteItemType } from '../../../entities/user-favorite.entity';
|
||||
|
||||
export class CreateFavoriteDto {
|
||||
@ApiProperty({ description: 'Item ID to favorite', example: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' })
|
||||
@IsUUID()
|
||||
itemId: string;
|
||||
|
||||
@ApiProperty({ description: 'Type of item', enum: FavoriteItemType, example: FavoriteItemType.PLACE })
|
||||
@IsEnum(FavoriteItemType)
|
||||
itemType: FavoriteItemType;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Item name for display', example: 'Zona Colonial' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
itemName?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Item image URL', example: 'https://example.com/image.jpg' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
itemImageUrl?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Additional item metadata' })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
itemMetadata?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'User notes', example: 'Must visit this place!' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
2
src/modules/favorites/dto/index.ts
Normal file
2
src/modules/favorites/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './create-favorite.dto';
|
||||
export * from './update-favorite.dto';
|
||||
14
src/modules/favorites/dto/update-favorite.dto.ts
Normal file
14
src/modules/favorites/dto/update-favorite.dto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsObject } from 'class-validator';
|
||||
|
||||
export class UpdateFavoriteDto {
|
||||
@ApiPropertyOptional({ description: 'User notes', example: 'Updated notes' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Additional item metadata' })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
itemMetadata?: Record<string, any>;
|
||||
}
|
||||
125
src/modules/favorites/favorites.controller.ts
Normal file
125
src/modules/favorites/favorites.controller.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam,
|
||||
} from '@nestjs/swagger';
|
||||
import { FavoritesService } from './favorites.service';
|
||||
import { CreateFavoriteDto } from './dto/create-favorite.dto';
|
||||
import { UpdateFavoriteDto } from './dto/update-favorite.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { UserFavorite, FavoriteItemType } from '../../entities/user-favorite.entity';
|
||||
|
||||
@ApiTags('Favorites')
|
||||
@Controller('favorites')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class FavoritesController {
|
||||
constructor(private readonly favoritesService: FavoritesService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Add item to favorites' })
|
||||
@ApiResponse({ status: 201, description: 'Item added to favorites', type: UserFavorite })
|
||||
@ApiResponse({ status: 409, description: 'Item already in favorites' })
|
||||
create(@Body() createFavoriteDto: CreateFavoriteDto, @Request() req) {
|
||||
return this.favoritesService.create(req.user.id, createFavoriteDto);
|
||||
}
|
||||
|
||||
@Get('my')
|
||||
@ApiOperation({ summary: 'Get current user favorites' })
|
||||
@ApiQuery({ name: 'type', required: false, enum: FavoriteItemType, description: 'Filter by item type' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiResponse({ status: 200, description: 'User favorites retrieved' })
|
||||
findMyFavorites(
|
||||
@Request() req,
|
||||
@Query('type') type?: FavoriteItemType,
|
||||
@Query('page') page?: number,
|
||||
@Query('limit') limit?: number,
|
||||
) {
|
||||
return this.favoritesService.findAllByUser(req.user.id, type, page, limit);
|
||||
}
|
||||
|
||||
@Get('my/counts')
|
||||
@ApiOperation({ summary: 'Get favorites count by type' })
|
||||
@ApiResponse({ status: 200, description: 'Favorites counts by type' })
|
||||
getFavoritesCounts(@Request() req) {
|
||||
return this.favoritesService.getFavoritesCount(req.user.id);
|
||||
}
|
||||
|
||||
@Get('check/:itemType/:itemId')
|
||||
@ApiOperation({ summary: 'Check if item is favorited' })
|
||||
@ApiParam({ name: 'itemType', enum: FavoriteItemType })
|
||||
@ApiParam({ name: 'itemId', type: 'string' })
|
||||
@ApiResponse({ status: 200, schema: { type: 'object', properties: { isFavorited: { type: 'boolean' } } } })
|
||||
checkFavorite(
|
||||
@Request() req,
|
||||
@Param('itemType') itemType: FavoriteItemType,
|
||||
@Param('itemId') itemId: string,
|
||||
) {
|
||||
return this.favoritesService.isFavorited(req.user.id, itemId, itemType).then(isFavorited => ({ isFavorited }));
|
||||
}
|
||||
|
||||
@Post('toggle')
|
||||
@ApiOperation({ summary: 'Toggle favorite status for an item' })
|
||||
@ApiResponse({ status: 200, description: 'Favorite toggled' })
|
||||
toggleFavorite(
|
||||
@Request() req,
|
||||
@Body() body: {
|
||||
itemId: string;
|
||||
itemType: FavoriteItemType;
|
||||
name?: string;
|
||||
imageUrl?: string;
|
||||
metadata?: Record<string, any>;
|
||||
},
|
||||
) {
|
||||
return this.favoritesService.toggleFavorite(
|
||||
req.user.id,
|
||||
body.itemId,
|
||||
body.itemType,
|
||||
{ name: body.name, imageUrl: body.imageUrl, metadata: body.metadata }
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get favorite by ID' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
@ApiResponse({ status: 200, type: UserFavorite })
|
||||
@ApiResponse({ status: 404, description: 'Favorite not found' })
|
||||
findOne(@Param('id') id: string, @Request() req) {
|
||||
return this.favoritesService.findOne(req.user.id, id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update favorite (notes, metadata)' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
@ApiResponse({ status: 200, type: UserFavorite })
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateFavoriteDto: UpdateFavoriteDto,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.favoritesService.update(req.user.id, id, updateFavoriteDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Remove from favorites' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
@ApiResponse({ status: 200, description: 'Removed from favorites' })
|
||||
remove(@Param('id') id: string, @Request() req) {
|
||||
return this.favoritesService.remove(req.user.id, id);
|
||||
}
|
||||
|
||||
@Delete('item/:itemType/:itemId')
|
||||
@ApiOperation({ summary: 'Remove from favorites by item ID and type' })
|
||||
@ApiParam({ name: 'itemType', enum: FavoriteItemType })
|
||||
@ApiParam({ name: 'itemId', type: 'string' })
|
||||
@ApiResponse({ status: 200, description: 'Removed from favorites' })
|
||||
removeByItem(
|
||||
@Param('itemType') itemType: FavoriteItemType,
|
||||
@Param('itemId') itemId: string,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.favoritesService.removeByItem(req.user.id, itemId, itemType);
|
||||
}
|
||||
}
|
||||
13
src/modules/favorites/favorites.module.ts
Normal file
13
src/modules/favorites/favorites.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FavoritesService } from './favorites.service';
|
||||
import { FavoritesController } from './favorites.controller';
|
||||
import { UserFavorite } from '../../entities/user-favorite.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserFavorite])],
|
||||
controllers: [FavoritesController],
|
||||
providers: [FavoritesService],
|
||||
exports: [FavoritesService],
|
||||
})
|
||||
export class FavoritesModule {}
|
||||
149
src/modules/favorites/favorites.service.ts
Normal file
149
src/modules/favorites/favorites.service.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserFavorite, FavoriteItemType } from '../../entities/user-favorite.entity';
|
||||
import { CreateFavoriteDto } from './dto/create-favorite.dto';
|
||||
import { UpdateFavoriteDto } from './dto/update-favorite.dto';
|
||||
|
||||
@Injectable()
|
||||
export class FavoritesService {
|
||||
constructor(
|
||||
@InjectRepository(UserFavorite)
|
||||
private readonly favoriteRepository: Repository<UserFavorite>,
|
||||
) {}
|
||||
|
||||
async create(userId: string, createFavoriteDto: CreateFavoriteDto): Promise<UserFavorite> {
|
||||
// Check if already favorited
|
||||
const existing = await this.favoriteRepository.findOne({
|
||||
where: {
|
||||
userId,
|
||||
itemId: createFavoriteDto.itemId,
|
||||
itemType: createFavoriteDto.itemType,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException('Item already in favorites');
|
||||
}
|
||||
|
||||
const favorite = this.favoriteRepository.create({
|
||||
userId,
|
||||
...createFavoriteDto,
|
||||
});
|
||||
|
||||
return this.favoriteRepository.save(favorite);
|
||||
}
|
||||
|
||||
async findAllByUser(
|
||||
userId: string,
|
||||
itemType?: FavoriteItemType,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
): Promise<{ data: UserFavorite[]; total: number; page: number; limit: number }> {
|
||||
const where: any = { userId };
|
||||
|
||||
if (itemType) {
|
||||
where.itemType = itemType;
|
||||
}
|
||||
|
||||
const [data, total] = await this.favoriteRepository.findAndCount({
|
||||
where,
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async findOne(userId: string, id: string): Promise<UserFavorite> {
|
||||
const favorite = await this.favoriteRepository.findOne({
|
||||
where: { id, userId },
|
||||
});
|
||||
|
||||
if (!favorite) {
|
||||
throw new NotFoundException('Favorite not found');
|
||||
}
|
||||
|
||||
return favorite;
|
||||
}
|
||||
|
||||
async update(userId: string, id: string, updateFavoriteDto: UpdateFavoriteDto): Promise<UserFavorite> {
|
||||
const favorite = await this.findOne(userId, id);
|
||||
|
||||
Object.assign(favorite, updateFavoriteDto);
|
||||
|
||||
return this.favoriteRepository.save(favorite);
|
||||
}
|
||||
|
||||
async remove(userId: string, id: string): Promise<void> {
|
||||
const favorite = await this.findOne(userId, id);
|
||||
await this.favoriteRepository.remove(favorite);
|
||||
}
|
||||
|
||||
async removeByItem(userId: string, itemId: string, itemType: FavoriteItemType): Promise<void> {
|
||||
const favorite = await this.favoriteRepository.findOne({
|
||||
where: { userId, itemId, itemType },
|
||||
});
|
||||
|
||||
if (favorite) {
|
||||
await this.favoriteRepository.remove(favorite);
|
||||
}
|
||||
}
|
||||
|
||||
async isFavorited(userId: string, itemId: string, itemType: FavoriteItemType): Promise<boolean> {
|
||||
const count = await this.favoriteRepository.count({
|
||||
where: { userId, itemId, itemType },
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
async getFavoritesCount(userId: string): Promise<Record<FavoriteItemType, number>> {
|
||||
const counts = await this.favoriteRepository
|
||||
.createQueryBuilder('favorite')
|
||||
.select('favorite.item_type', 'itemType')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('favorite.user_id = :userId', { userId })
|
||||
.groupBy('favorite.item_type')
|
||||
.getRawMany();
|
||||
|
||||
const result: Record<string, number> = {};
|
||||
Object.values(FavoriteItemType).forEach(type => {
|
||||
result[type] = 0;
|
||||
});
|
||||
|
||||
counts.forEach(c => {
|
||||
result[c.itemType] = parseInt(c.count, 10);
|
||||
});
|
||||
|
||||
return result as Record<FavoriteItemType, number>;
|
||||
}
|
||||
|
||||
async toggleFavorite(
|
||||
userId: string,
|
||||
itemId: string,
|
||||
itemType: FavoriteItemType,
|
||||
itemData?: { name?: string; imageUrl?: string; metadata?: Record<string, any> }
|
||||
): Promise<{ isFavorited: boolean; favorite?: UserFavorite }> {
|
||||
const existing = await this.favoriteRepository.findOne({
|
||||
where: { userId, itemId, itemType },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await this.favoriteRepository.remove(existing);
|
||||
return { isFavorited: false };
|
||||
}
|
||||
|
||||
const favorite = this.favoriteRepository.create({
|
||||
userId,
|
||||
itemId,
|
||||
itemType,
|
||||
itemName: itemData?.name,
|
||||
itemImageUrl: itemData?.imageUrl,
|
||||
itemMetadata: itemData?.metadata,
|
||||
});
|
||||
|
||||
const saved = await this.favoriteRepository.save(favorite);
|
||||
return { isFavorited: true, favorite: saved };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user