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:
99
apps/orchestrator/src/api/health/health.controller.spec.ts
Normal file
99
apps/orchestrator/src/api/health/health.controller.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
14
apps/orchestrator/src/api/health/health.service.ts
Normal file
14
apps/orchestrator/src/api/health/health.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user