Files
recipe-app/backend/src/products/products.controller.ts
T
Nils-Johan Gynther f19c157e8f
Test Suite / test (24.15.0) (push) Has been cancelled
feat: add updateCategoryMine endpoint to manage product category updates
- Implemented a new PATCH endpoint in ProductsController to update the category of a user's product.
- Added corresponding service method in ProductsService to handle business logic and validation.
- Created UpdateCategoryMineDto for request validation.
- Enhanced error handling for forbidden actions and not found resources.
- Updated API error mapping in Flutter to handle specific forbidden messages.
- Modified ProductPickerField to allow product creation directly from the picker.
- Added tests for the new endpoint and service method to ensure proper functionality and error handling.
2026-05-11 21:41:42 +02:00

270 lines
7.4 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
ParseIntPipe,
Patch,
Post,
Put,
Query,
Request,
UseGuards,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { Public } from '../auth/decorators/public.decorator';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
import { ProductsService } from './products.service';
import { MergeProductsDto } from './dto/merge-products.dto';
import { UpdateCanonicalNameDto } from './dto/update-canonical-name.dto';
import { SetTagsDto } from './dto/set-tags.dto';
import { UpsertNutritionDto } from './dto/upsert-nutrition.dto';
import { BulkUpdateProductsDto } from './dto/bulk-update-products.dto';
import { AiCategorizeBulkDto } from './dto/ai-categorize-bulk.dto';
import { SetProductStatusDto } from './dto/set-product-status.dto';
import { UpdateCategoryMineDto } from './dto/update-category-mine.dto';
import { Roles } from '../auth/decorators/roles.decorator';
import { AiService } from '../ai/ai.service';
import { CategoriesService } from '../categories/categories.service';
import { PremiumOrAdminGuard } from '../auth/premium-or-admin.guard';
@Controller('products')
export class ProductsController {
constructor(
private readonly productsService: ProductsService,
private readonly aiService: AiService,
private readonly categoriesService: CategoriesService,
) {}
@Public()
@Get()
findAll(
@Query('tag') tag?: string,
) {
return this.productsService.findAll({ tag });
}
@Public()
@Get('tags')
findAllTags() {
return this.productsService.findAllTags();
}
@Roles('admin')
@Get('duplicates')
findDuplicates() {
return this.productsService.findDuplicateCandidates();
}
@Roles('admin')
@Get('merge-preview')
previewMerge(
@Query('sourceProductId', ParseIntPipe) sourceProductId: number,
@Query('targetProductId', ParseIntPipe) targetProductId: number,
) {
return this.productsService.previewMerge(sourceProductId, targetProductId);
}
@Roles('admin')
@Post('backfill-canonical')
backfillCanonical() {
return this.productsService.backfillCanonicalNames();
}
@Roles('admin')
@Get('pending')
findPending() {
return this.productsService.findPending();
}
@Roles('admin')
@Get('private')
findPrivate() {
return this.productsService.findPrivate();
}
@Roles('admin')
@Post('ai-categorize-bulk')
@Throttle({ default: { ttl: 60_000, limit: 5 } })
@HttpCode(200)
aiCategorizeBulk(@Body() body: AiCategorizeBulkDto) {
return this.productsService.aiCategorizeBulk(body.productIds);
}
@Roles('admin')
@Get('deleted')
findDeleted() {
return this.productsService.findDeleted();
}
// Inloggad användares egna privata produkter (måste vara före :id)
@Get('mine')
findMine(@Request() req: { user: { id: number } }) {
return this.productsService.findByOwner(req.user.id);
}
// Tillgänglig för alla inloggade användare
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.productsService.findOne(id);
}
// Skapa en privat produkt för den inloggade användaren
@Post('private')
createPrivate(
@Body() body: CreateProductDto,
@Request() req: { user: { id: number } },
) {
return this.productsService.createPrivate(body, req.user.id);
}
@UseGuards(PremiumOrAdminGuard)
@Get(':id/suggest-category')
@Throttle({ default: { ttl: 60_000, limit: 20 } })
async suggestCategory(
@Param('id', ParseIntPipe) id: number,
) {
const product = await this.productsService.findOne(id);
const categories = await this.categoriesService.findFlattened();
return this.aiService.suggestCategory(product.canonicalName ?? product.name, categories);
}
@Roles('admin')
@Post()
create(@Body() body: CreateProductDto, @Request() req: { user: { id: number } }) {
return this.productsService.create(body, req.user.id);
}
// Tillgänglig för alla inloggade användare — req.user.id injiceras av JWT-guard
@Post('pending')
createPending(
@Body() body: CreateProductDto,
@Request() req: { user: { id: number } },
) {
return this.productsService.createPending(body, req.user.id);
}
// ── Privata produkter: rename & merge ──────────────────────────────────────
// Inloggade användare kan hantera sina egna privata produkter
@Patch('private/:id/canonical-name')
updateCanonicalNamePrivate(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateCanonicalNameDto,
@Request() req: { user: { id: number } },
) {
return this.productsService.updateCanonicalNamePrivate(req.user.id, id, body.canonicalName);
}
@Post('private/merge')
mergePrivate(
@Body() body: MergeProductsDto,
@Request() req: { user: { id: number } },
) {
return this.productsService.mergePrivate(req.user.id, body.sourceProductId, body.targetProductId);
}
@Patch('mine/:id/category')
updateCategoryMine(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateCategoryMineDto,
@Request() req: { user: { id: number } },
) {
return this.productsService.updateCategoryMine(req.user.id, id, body.categoryId);
}
@Roles('admin')
@Post('merge')
merge(@Body() body: MergeProductsDto) {
return this.productsService.merge(body.sourceProductId, body.targetProductId);
}
@Roles('admin')
@Patch(':id/canonical-name')
updateCanonicalName(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateCanonicalNameDto,
) {
return this.productsService.updateCanonicalName(id, body.canonicalName);
}
@Roles('admin')
@Put(':id/tags')
setTags(
@Param('id', ParseIntPipe) id: number,
@Body() body: SetTagsDto,
) {
return this.productsService.setTags(id, body.tags);
}
@Roles('admin')
@Put(':id/nutrition')
upsertNutrition(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpsertNutritionDto,
) {
return this.productsService.upsertNutrition(id, body);
}
@Roles('admin')
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateProductDto,
) {
return this.productsService.update(id, body);
}
@Roles('admin')
@Post('private/:id/promote')
promotePrivateToGlobal(
@Param('id', ParseIntPipe) id: number,
@Request() req: { user: { id: number } },
) {
return this.productsService.promotePrivateToGlobal(id, req.user.id);
}
@Roles('admin')
@Delete(':id/permanent')
permanentDelete(@Param('id', ParseIntPipe) id: number) {
return this.productsService.permanentDelete(id);
}
@Roles('admin')
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.productsService.remove(id);
}
@Roles('admin')
@Patch(':id/status')
setStatus(
@Param('id', ParseIntPipe) id: number,
@Body() body: SetProductStatusDto,
) {
return this.productsService.setStatus(id, body.status);
}
@Roles('admin')
@Post(':id/restore')
restore(@Param('id', ParseIntPipe) id: number) {
return this.productsService.restore(id);
}
@Roles('admin')
@Post('reset-all')
@HttpCode(200)
resetAll() {
return this.productsService.resetAll();
}
@Roles('admin')
@Post('bulk-update')
@HttpCode(200)
bulkUpdate(@Body() body: BulkUpdateProductsDto) {
return this.productsService.bulkUpdate(body.ids, { categoryId: body.categoryId });
}
}