feat(11-06): TenderSavedSearch model + CRUD service (FILTER-06)

GREEN phase — adds TenderSavedSearch (userId+tenantId scoped, filters
Json, @@unique([userId,name])), the migration (applied to local dev DB),
and TenderSavedSearchService following the FavoritesService/
TenderTriageService pattern: manual where:{userId} scoping (no
forTenant()/RLS), ownership check before update/remove, P2002 unique
conflicts translated to ConflictException.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 16:45:04 +02:00
parent 2b0b4f897b
commit df94d921ef
4 changed files with 198 additions and 0 deletions
@@ -0,0 +1,41 @@
import { IsNotEmpty, IsObject, IsOptional, IsString, MaxLength } from 'class-validator';
/**
* Body DTO for POST /modules/tender-radar/saved-searches (FILTER-06).
*
* Security (T-11-14 / V4 — IDOR): deliberately has NO userId or tenantId
* field — both are always derived server-side from the auth context
* (FavoritesController.extractContext pattern) in TendersController, never
* trusted from the request body.
*
* `filters` is validated only as a plain object (T-11-17 / V5) — its shape
* mirrors the FilterPanel's URL-searchParams contract on the frontend, but
* the backend does not re-validate individual filter keys here; Prisma
* stores it as parametrized JSONB (no string interpolation, V5).
*/
export class CreateSavedSearchDto {
@IsString()
@IsNotEmpty()
@MaxLength(100)
name!: string;
@IsObject()
filters!: Record<string, unknown>;
}
/**
* Body DTO for PATCH /modules/tender-radar/saved-searches/:searchId.
* Both fields optional — only provided fields are updated (FavoritesService
* UpdateFavoriteDto pattern).
*/
export class UpdateSavedSearchDto {
@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(100)
name?: string;
@IsOptional()
@IsObject()
filters?: Record<string, unknown>;
}