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:
2026-03-17 11:47:37 -04:00
parent 6e0ad420ab
commit 8b6483aa7d
971 changed files with 16339 additions and 752 deletions

View 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;
}

View File

@@ -0,0 +1,2 @@
export * from './create-favorite.dto';
export * from './update-favorite.dto';

View 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>;
}

View 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);
}
}

View 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 {}

View 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 };
}
}