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:
Jason Woltje
2026-02-02 14:33:31 -06:00
parent 24d59e7595
commit c3500783d1
121 changed files with 4123 additions and 58 deletions

View File

@@ -0,0 +1,99 @@
import { describe, it, expect, beforeEach } from "vitest";
import { HealthController } from "./health.controller";
import { HealthService } from "./health.service";
describe("HealthController", () => {
let controller: HealthController;
let service: HealthService;
beforeEach(() => {
service = new HealthService();
controller = new HealthController(service);
});
describe("GET /health", () => {
it("should return 200 OK with correct format", () => {
const result = controller.check();
expect(result).toBeDefined();
expect(result).toHaveProperty("status");
expect(result).toHaveProperty("uptime");
expect(result).toHaveProperty("timestamp");
});
it('should return status as "healthy"', () => {
const result = controller.check();
expect(result.status).toBe("healthy");
});
it("should return uptime as a positive number", () => {
const result = controller.check();
expect(typeof result.uptime).toBe("number");
expect(result.uptime).toBeGreaterThanOrEqual(0);
});
it("should return timestamp as valid ISO 8601 string", () => {
const result = controller.check();
expect(typeof result.timestamp).toBe("string");
expect(() => new Date(result.timestamp)).not.toThrow();
// Verify it's a valid ISO 8601 format
const date = new Date(result.timestamp);
expect(date.toISOString()).toBe(result.timestamp);
});
it("should return only required fields (status, uptime, timestamp)", () => {
const result = controller.check();
const keys = Object.keys(result);
expect(keys).toHaveLength(3);
expect(keys).toContain("status");
expect(keys).toContain("uptime");
expect(keys).toContain("timestamp");
});
it("should increment uptime over time", async () => {
const result1 = controller.check();
const uptime1 = result1.uptime;
// Wait 1100ms to ensure at least 1 second has passed
await new Promise((resolve) => setTimeout(resolve, 1100));
const result2 = controller.check();
const uptime2 = result2.uptime;
// Uptime should be at least 1 second higher
expect(uptime2).toBeGreaterThanOrEqual(uptime1 + 1);
});
it("should return current timestamp", () => {
const before = Date.now();
const result = controller.check();
const after = Date.now();
const resultTime = new Date(result.timestamp).getTime();
// Timestamp should be between before and after (within test execution time)
expect(resultTime).toBeGreaterThanOrEqual(before);
expect(resultTime).toBeLessThanOrEqual(after);
});
});
describe("GET /health/ready", () => {
it("should return ready status", () => {
const result = controller.ready();
expect(result).toBeDefined();
expect(result).toHaveProperty("ready");
});
it("should return ready as true", () => {
const result = controller.ready();
expect(result.ready).toBe(true);
});
});
});

View File

@@ -1,13 +1,15 @@
import { Controller, Get } from "@nestjs/common";
import { HealthService } from "./health.service";
@Controller("health")
export class HealthController {
constructor(private readonly healthService: HealthService) {}
@Get()
check() {
return {
status: "ok",
service: "orchestrator",
version: "0.0.6",
status: "healthy",
uptime: this.healthService.getUptime(),
timestamp: new Date().toISOString(),
};
}

View File

@@ -0,0 +1,14 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class HealthService {
private readonly startTime: number;
constructor() {
this.startTime = Date.now();
}
getUptime(): number {
return Math.floor((Date.now() - this.startTime) / 1000);
}
}