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:
46
apps/api/src/knowledge/dto/create-entry.dto.ts
Normal file
46
apps/api/src/knowledge/dto/create-entry.dto.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsEnum,
|
||||
IsArray,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from "class-validator";
|
||||
import { EntryStatus, Visibility } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* DTO for creating a new knowledge entry
|
||||
*/
|
||||
export class CreateEntryDto {
|
||||
@IsString({ message: "title must be a string" })
|
||||
@MinLength(1, { message: "title must not be empty" })
|
||||
@MaxLength(500, { message: "title must not exceed 500 characters" })
|
||||
title!: string;
|
||||
|
||||
@IsString({ message: "content must be a string" })
|
||||
@MinLength(1, { message: "content must not be empty" })
|
||||
content!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "summary must be a string" })
|
||||
@MaxLength(1000, { message: "summary must not exceed 1000 characters" })
|
||||
summary?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(EntryStatus, { message: "status must be a valid EntryStatus" })
|
||||
status?: EntryStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(Visibility, { message: "visibility must be a valid Visibility" })
|
||||
visibility?: Visibility;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray({ message: "tags must be an array" })
|
||||
@IsString({ each: true, message: "each tag must be a string" })
|
||||
tags?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "changeNote must be a string" })
|
||||
@MaxLength(500, { message: "changeNote must not exceed 500 characters" })
|
||||
changeNote?: string;
|
||||
}
|
||||
39
apps/api/src/knowledge/dto/create-tag.dto.ts
Normal file
39
apps/api/src/knowledge/dto/create-tag.dto.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
Matches,
|
||||
} from "class-validator";
|
||||
|
||||
/**
|
||||
* DTO for creating a new knowledge tag
|
||||
*/
|
||||
export class CreateTagDto {
|
||||
@IsString({ message: "name must be a string" })
|
||||
@MinLength(1, { message: "name must not be empty" })
|
||||
@MaxLength(100, { message: "name must not exceed 100 characters" })
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "slug must be a string" })
|
||||
@MinLength(1, { message: "slug must not be empty" })
|
||||
@MaxLength(100, { message: "slug must not exceed 100 characters" })
|
||||
@Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
|
||||
message:
|
||||
"slug must be lowercase, alphanumeric, and may contain hyphens (e.g., 'my-tag-name')",
|
||||
})
|
||||
slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "color must be a string" })
|
||||
@Matches(/^#[0-9A-Fa-f]{6}$/, {
|
||||
message: "color must be a valid hex color (e.g., #FF5733)",
|
||||
})
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "description must be a string" })
|
||||
@MaxLength(500, { message: "description must not exceed 500 characters" })
|
||||
description?: string;
|
||||
}
|
||||
29
apps/api/src/knowledge/dto/entry-query.dto.ts
Normal file
29
apps/api/src/knowledge/dto/entry-query.dto.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { IsOptional, IsEnum, IsString, IsInt, Min, Max } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { EntryStatus } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* DTO for querying knowledge entries (list endpoint)
|
||||
*/
|
||||
export class EntryQueryDto {
|
||||
@IsOptional()
|
||||
@IsEnum(EntryStatus, { message: "status must be a valid EntryStatus" })
|
||||
status?: EntryStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "tag must be a string" })
|
||||
tag?: string;
|
||||
|
||||
@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;
|
||||
}
|
||||
5
apps/api/src/knowledge/dto/index.ts
Normal file
5
apps/api/src/knowledge/dto/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { CreateEntryDto } from "./create-entry.dto";
|
||||
export { UpdateEntryDto } from "./update-entry.dto";
|
||||
export { EntryQueryDto } from "./entry-query.dto";
|
||||
export { CreateTagDto } from "./create-tag.dto";
|
||||
export { UpdateTagDto } from "./update-tag.dto";
|
||||
48
apps/api/src/knowledge/dto/update-entry.dto.ts
Normal file
48
apps/api/src/knowledge/dto/update-entry.dto.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsEnum,
|
||||
IsArray,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from "class-validator";
|
||||
import { EntryStatus, Visibility } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* DTO for updating a knowledge entry
|
||||
*/
|
||||
export class UpdateEntryDto {
|
||||
@IsOptional()
|
||||
@IsString({ message: "title must be a string" })
|
||||
@MinLength(1, { message: "title must not be empty" })
|
||||
@MaxLength(500, { message: "title must not exceed 500 characters" })
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "content must be a string" })
|
||||
@MinLength(1, { message: "content must not be empty" })
|
||||
content?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "summary must be a string" })
|
||||
@MaxLength(1000, { message: "summary must not exceed 1000 characters" })
|
||||
summary?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(EntryStatus, { message: "status must be a valid EntryStatus" })
|
||||
status?: EntryStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(Visibility, { message: "visibility must be a valid Visibility" })
|
||||
visibility?: Visibility;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray({ message: "tags must be an array" })
|
||||
@IsString({ each: true, message: "each tag must be a string" })
|
||||
tags?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "changeNote must be a string" })
|
||||
@MaxLength(500, { message: "changeNote must not exceed 500 characters" })
|
||||
changeNote?: string;
|
||||
}
|
||||
31
apps/api/src/knowledge/dto/update-tag.dto.ts
Normal file
31
apps/api/src/knowledge/dto/update-tag.dto.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
Matches,
|
||||
} from "class-validator";
|
||||
|
||||
/**
|
||||
* DTO for updating an existing knowledge tag
|
||||
* All fields are optional to support partial updates
|
||||
*/
|
||||
export class UpdateTagDto {
|
||||
@IsOptional()
|
||||
@IsString({ message: "name must be a string" })
|
||||
@MinLength(1, { message: "name must not be empty" })
|
||||
@MaxLength(100, { message: "name must not exceed 100 characters" })
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "color must be a string" })
|
||||
@Matches(/^#[0-9A-Fa-f]{6}$/, {
|
||||
message: "color must be a valid hex color (e.g., #FF5733)",
|
||||
})
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "description must be a string" })
|
||||
@MaxLength(500, { message: "description must not exceed 500 characters" })
|
||||
description?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user