feat(knowledge): add tag management API (KNOW-003)
- Add Tag DTOs (CreateTagDto, UpdateTagDto) with validation - Implement TagsService with CRUD operations - Add TagsController with authenticated endpoints - Support automatic slug generation from tag names - Add workspace isolation for tags - Include entry count in tag responses - Add findOrCreateTags method for entry creation/update - Implement comprehensive test coverage (29 tests passing) Endpoints: - GET /api/knowledge/tags - List workspace tags - POST /api/knowledge/tags - Create tag - GET /api/knowledge/tags/:slug - Get tag by slug - PUT /api/knowledge/tags/:slug - Update tag - DELETE /api/knowledge/tags/:slug - Delete tag - GET /api/knowledge/tags/:slug/entries - List entries with tag Related: KNOW-003
This commit is contained in:
160
apps/api/src/knowledge/knowledge.controller.ts
Normal file
160
apps/api/src/knowledge/knowledge.controller.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
} from "@nestjs/swagger";
|
||||
import type { AuthUser } from "@mosaic/shared";
|
||||
import { KnowledgeService } from "./knowledge.service";
|
||||
import { CreateEntryDto, UpdateEntryDto, EntryQueryDto } from "./dto";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { CurrentUser } from "../auth/decorators/current-user.decorator";
|
||||
|
||||
/**
|
||||
* Controller for knowledge entry endpoints
|
||||
* All endpoints require authentication and enforce workspace isolation
|
||||
*/
|
||||
@ApiTags("knowledge")
|
||||
@ApiBearerAuth()
|
||||
@Controller("knowledge/entries")
|
||||
@UseGuards(AuthGuard)
|
||||
export class KnowledgeController {
|
||||
constructor(private readonly knowledgeService: KnowledgeService) {}
|
||||
|
||||
/**
|
||||
* GET /api/knowledge/entries
|
||||
* List all entries in the workspace with pagination and filtering
|
||||
*/
|
||||
@Get()
|
||||
@ApiOperation({ summary: "List knowledge entries" })
|
||||
@ApiQuery({ name: "status", required: false, enum: ["DRAFT", "PUBLISHED", "ARCHIVED"] })
|
||||
@ApiQuery({ name: "tag", required: false, type: String })
|
||||
@ApiQuery({ name: "page", required: false, type: Number })
|
||||
@ApiQuery({ name: "limit", required: false, type: Number })
|
||||
@ApiResponse({ status: 200, description: "Returns paginated list of entries" })
|
||||
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||
async findAll(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Query() query: EntryQueryDto
|
||||
) {
|
||||
const workspaceId = user?.workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException("Workspace context required");
|
||||
}
|
||||
|
||||
return this.knowledgeService.findAll(workspaceId, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/knowledge/entries/:slug
|
||||
* Get a single entry by slug
|
||||
*/
|
||||
@Get(":slug")
|
||||
@ApiOperation({ summary: "Get knowledge entry by slug" })
|
||||
@ApiParam({ name: "slug", type: String })
|
||||
@ApiResponse({ status: 200, description: "Returns the entry" })
|
||||
@ApiResponse({ status: 404, description: "Entry not found" })
|
||||
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||
async findOne(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param("slug") slug: string
|
||||
) {
|
||||
const workspaceId = user?.workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new UnauthorizedException("Workspace context required");
|
||||
}
|
||||
|
||||
return this.knowledgeService.findOne(workspaceId, slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/knowledge/entries
|
||||
* Create a new knowledge entry
|
||||
*/
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Create a new knowledge entry" })
|
||||
@ApiResponse({ status: 201, description: "Entry created successfully" })
|
||||
@ApiResponse({ status: 400, description: "Validation error" })
|
||||
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||
@ApiResponse({ status: 409, description: "Slug conflict" })
|
||||
async create(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() createDto: CreateEntryDto
|
||||
) {
|
||||
const workspaceId = user?.workspaceId;
|
||||
const userId = user?.id;
|
||||
|
||||
if (!workspaceId || !userId) {
|
||||
throw new UnauthorizedException("Authentication required");
|
||||
}
|
||||
|
||||
return this.knowledgeService.create(workspaceId, userId, createDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/knowledge/entries/:slug
|
||||
* Update an existing entry
|
||||
*/
|
||||
@Put(":slug")
|
||||
@ApiOperation({ summary: "Update a knowledge entry" })
|
||||
@ApiParam({ name: "slug", type: String })
|
||||
@ApiResponse({ status: 200, description: "Entry updated successfully" })
|
||||
@ApiResponse({ status: 400, description: "Validation error" })
|
||||
@ApiResponse({ status: 404, description: "Entry not found" })
|
||||
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||
async update(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param("slug") slug: string,
|
||||
@Body() updateDto: UpdateEntryDto
|
||||
) {
|
||||
const workspaceId = user?.workspaceId;
|
||||
const userId = user?.id;
|
||||
|
||||
if (!workspaceId || !userId) {
|
||||
throw new UnauthorizedException("Authentication required");
|
||||
}
|
||||
|
||||
return this.knowledgeService.update(workspaceId, slug, userId, updateDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/knowledge/entries/:slug
|
||||
* Soft delete an entry (sets status to ARCHIVED)
|
||||
*/
|
||||
@Delete(":slug")
|
||||
@ApiOperation({ summary: "Delete a knowledge entry (soft delete)" })
|
||||
@ApiParam({ name: "slug", type: String })
|
||||
@ApiResponse({ status: 204, description: "Entry archived successfully" })
|
||||
@ApiResponse({ status: 404, description: "Entry not found" })
|
||||
@ApiResponse({ status: 401, description: "Unauthorized" })
|
||||
async remove(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param("slug") slug: string
|
||||
) {
|
||||
const workspaceId = user?.workspaceId;
|
||||
const userId = user?.id;
|
||||
|
||||
if (!workspaceId || !userId) {
|
||||
throw new UnauthorizedException("Authentication required");
|
||||
}
|
||||
|
||||
await this.knowledgeService.remove(workspaceId, slug, userId);
|
||||
return { message: "Entry archived successfully" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user