Files
stack/apps/api/src/knowledge/dto/search-query.dto.ts
T
Jason WoltjeandClaude Sonnet 4.5 c3500783d1 feat(#66): implement tag filtering in search API endpoint
Add support for filtering search results by tags in the main search endpoint.

Changes:
- Add tags parameter to SearchQueryDto (comma-separated tag slugs)
- Implement tag filtering in SearchService.search() method
- Update SQL query to join with knowledge_entry_tags when tags provided
- Entries must have ALL specified tags (AND logic)
- Add tests for tag filtering (2 controller tests, 2 service tests)
- Update endpoint documentation
- Fix non-null assertion linting error

The search endpoint now supports:
- Full-text search with ranking (ts_rank)
- Snippet generation with highlighting (ts_headline)
- Status filtering
- Tag filtering (new)
- Pagination

Example: GET /api/knowledge/search?q=api&tags=documentation,tutorial

All tests pass (25 total), type checking passes, linting passes.

Fixes #66

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-02-02 14:33:31 -06:00

78 lines
2.2 KiB
TypeScript

import { IsOptional, IsString, IsInt, Min, Max, IsArray, IsEnum } from "class-validator";
import { Type, Transform } from "class-transformer";
import { EntryStatus } from "@prisma/client";
/**
* DTO for full-text search query parameters
*/
export class SearchQueryDto {
@IsString({ message: "q (query) must be a string" })
q!: string;
@IsOptional()
@Transform(({ value }) => (typeof value === "string" ? value.split(",") : (value as string[])))
@IsArray({ message: "tags must be an array" })
@IsString({ each: true, message: "each tag must be a string" })
tags?: string[];
@IsOptional()
@IsEnum(EntryStatus, { message: "status must be a valid EntryStatus" })
status?: EntryStatus;
@IsOptional()
@Type(() => Number)
@IsInt({ message: "page must be an integer" })
@Min(1, { message: "page must be at least 1" })
page?: number;
@IsOptional()
@Type(() => Number)
@IsInt({ message: "limit must be an integer" })
@Min(1, { message: "limit must be at least 1" })
@Max(100, { message: "limit must not exceed 100" })
limit?: number;
}
/**
* DTO for searching by tags
*/
export class TagSearchDto {
@Transform(({ value }) => (typeof value === "string" ? value.split(",") : (value as string[])))
@IsArray({ message: "tags must be an array" })
@IsString({ each: true, message: "each tag must be a string" })
tags!: string[];
@IsOptional()
@IsEnum(EntryStatus, { message: "status must be a valid EntryStatus" })
status?: EntryStatus;
@IsOptional()
@Type(() => Number)
@IsInt({ message: "page must be an integer" })
@Min(1, { message: "page must be at least 1" })
page?: number;
@IsOptional()
@Type(() => Number)
@IsInt({ message: "limit must be an integer" })
@Min(1, { message: "limit must be at least 1" })
@Max(100, { message: "limit must not exceed 100" })
limit?: number;
}
/**
* DTO for recent entries query
*/
export class RecentEntriesDto {
@IsOptional()
@Type(() => Number)
@IsInt({ message: "limit must be an integer" })
@Min(1, { message: "limit must be at least 1" })
@Max(50, { message: "limit must not exceed 50" })
limit?: number;
@IsOptional()
@IsEnum(EntryStatus, { message: "status must be a valid EntryStatus" })
status?: EntryStatus;
}