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 <noreply@anthropic.com>
This commit is contained in:
@@ -55,15 +55,11 @@ describe("SearchController", () => {
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(mockSearchService.search).toHaveBeenCalledWith(
|
||||
"test",
|
||||
mockWorkspaceId,
|
||||
{
|
||||
status: undefined,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
}
|
||||
);
|
||||
expect(mockSearchService.search).toHaveBeenCalledWith("test", mockWorkspaceId, {
|
||||
status: undefined,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
});
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
|
||||
@@ -79,15 +75,54 @@ describe("SearchController", () => {
|
||||
status: EntryStatus.PUBLISHED,
|
||||
});
|
||||
|
||||
expect(mockSearchService.search).toHaveBeenCalledWith(
|
||||
"test",
|
||||
mockWorkspaceId,
|
||||
{
|
||||
status: EntryStatus.PUBLISHED,
|
||||
page: undefined,
|
||||
limit: undefined,
|
||||
}
|
||||
);
|
||||
expect(mockSearchService.search).toHaveBeenCalledWith("test", mockWorkspaceId, {
|
||||
status: EntryStatus.PUBLISHED,
|
||||
page: undefined,
|
||||
limit: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should pass tags filter to service", async () => {
|
||||
mockSearchService.search.mockResolvedValue({
|
||||
data: [],
|
||||
pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
|
||||
query: "test",
|
||||
});
|
||||
|
||||
await controller.search(mockWorkspaceId, {
|
||||
q: "test",
|
||||
tags: ["api", "documentation"],
|
||||
});
|
||||
|
||||
expect(mockSearchService.search).toHaveBeenCalledWith("test", mockWorkspaceId, {
|
||||
status: undefined,
|
||||
page: undefined,
|
||||
limit: undefined,
|
||||
tags: ["api", "documentation"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should pass both status and tags filters to service", async () => {
|
||||
mockSearchService.search.mockResolvedValue({
|
||||
data: [],
|
||||
pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
|
||||
query: "test",
|
||||
});
|
||||
|
||||
await controller.search(mockWorkspaceId, {
|
||||
q: "test",
|
||||
status: EntryStatus.PUBLISHED,
|
||||
tags: ["api"],
|
||||
page: 2,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
expect(mockSearchService.search).toHaveBeenCalledWith("test", mockWorkspaceId, {
|
||||
status: EntryStatus.PUBLISHED,
|
||||
page: 2,
|
||||
limit: 10,
|
||||
tags: ["api"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,15 +163,11 @@ describe("SearchController", () => {
|
||||
status: EntryStatus.DRAFT,
|
||||
});
|
||||
|
||||
expect(mockSearchService.searchByTags).toHaveBeenCalledWith(
|
||||
["api"],
|
||||
mockWorkspaceId,
|
||||
{
|
||||
status: EntryStatus.DRAFT,
|
||||
page: undefined,
|
||||
limit: undefined,
|
||||
}
|
||||
);
|
||||
expect(mockSearchService.searchByTags).toHaveBeenCalledWith(["api"], mockWorkspaceId, {
|
||||
status: EntryStatus.DRAFT,
|
||||
page: undefined,
|
||||
limit: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,11 +187,7 @@ describe("SearchController", () => {
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
expect(mockSearchService.recentEntries).toHaveBeenCalledWith(
|
||||
mockWorkspaceId,
|
||||
10,
|
||||
undefined
|
||||
);
|
||||
expect(mockSearchService.recentEntries).toHaveBeenCalledWith(mockWorkspaceId, 10, undefined);
|
||||
expect(result).toEqual({
|
||||
data: mockEntries,
|
||||
count: 1,
|
||||
@@ -172,11 +199,7 @@ describe("SearchController", () => {
|
||||
|
||||
await controller.recentEntries(mockWorkspaceId, {});
|
||||
|
||||
expect(mockSearchService.recentEntries).toHaveBeenCalledWith(
|
||||
mockWorkspaceId,
|
||||
10,
|
||||
undefined
|
||||
);
|
||||
expect(mockSearchService.recentEntries).toHaveBeenCalledWith(mockWorkspaceId, 10, undefined);
|
||||
});
|
||||
|
||||
it("should pass status filter to service", async () => {
|
||||
|
||||
Reference in New Issue
Block a user