feat: Update TableOfContent Handler

This commit is contained in:
Buravit Yenjit 2025-04-04 01:19:14 +07:00
parent 5eec21a2b1
commit 1dee8e7f97
6 changed files with 192 additions and 13 deletions

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2"
@ -65,6 +66,13 @@ func (a *api) registerKnowledgeHubRoutes(_ chi.Router, api huma.API) {
Path: prefix + "/{uuid}/related", Path: prefix + "/{uuid}/related",
Tags: tags, Tags: tags,
}, a.createRelatedArticleHandler) }, a.createRelatedArticleHandler)
huma.Register(api, huma.Operation{
OperationID: "generateTableOfContents",
Method: http.MethodPost,
Path: prefix + "/{uuid}/generate-toc",
Tags: tags,
}, a.generateTOCHandler)
} }
type GetKnowledgeArticlesOutput struct { type GetKnowledgeArticlesOutput struct {
@ -118,6 +126,16 @@ type CreateRelatedArticleInput struct {
} `json:"body"` } `json:"body"`
} }
type GenerateTOCInput struct {
UUID string `path:"uuid"`
}
type GenerateTOCOutput struct {
Body struct {
TableOfContents []domain.TableOfContent `json:"table_of_contents"`
} `json:"body"`
}
func (a *api) getAllKnowledgeArticlesHandler(ctx context.Context, input *struct{}) (*GetKnowledgeArticlesOutput, error) { func (a *api) getAllKnowledgeArticlesHandler(ctx context.Context, input *struct{}) (*GetKnowledgeArticlesOutput, error) {
resp := &GetKnowledgeArticlesOutput{} resp := &GetKnowledgeArticlesOutput{}
@ -285,3 +303,70 @@ func (a *api) createRelatedArticleHandler(
return nil, nil return nil, nil
} }
func generateTOCFromContent(content string) []domain.TableOfContent {
var toc []domain.TableOfContent
lines := strings.Split(content, "\n")
order := 0
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "# ") {
order++
toc = append(toc, domain.TableOfContent{
Title: strings.TrimPrefix(line, "# "),
Level: 1,
Order: order,
})
} else if strings.HasPrefix(line, "## ") {
order++
toc = append(toc, domain.TableOfContent{
Title: strings.TrimPrefix(line, "## "),
Level: 2,
Order: order,
})
} else if strings.HasPrefix(line, "### ") {
order++
toc = append(toc, domain.TableOfContent{
Title: strings.TrimPrefix(line, "### "),
Level: 3,
Order: order,
})
}
// Add more levels if needed
}
return toc
}
func (a *api) generateTOCHandler(
ctx context.Context,
input *GenerateTOCInput,
) (*GenerateTOCOutput, error) {
resp := &GenerateTOCOutput{}
// Validate UUID format
if _, err := uuid.FromString(input.UUID); err != nil {
return nil, huma.Error400BadRequest("invalid UUID format")
}
// Get the article
article, err := a.knowledgeHubRepo.GetArticleByID(ctx, input.UUID)
if err != nil {
if errors.Is(err, domain.ErrNotFound) {
return nil, huma.Error404NotFound("article not found")
}
return nil, err
}
// Generate TOC from content
tocItems := generateTOCFromContent(article.Content)
// Save to database
if err := a.knowledgeHubRepo.CreateTableOfContents(ctx, input.UUID, tocItems); err != nil {
return nil, huma.Error500InternalServerError("failed to save table of contents")
}
resp.Body.TableOfContents = tocItems
return resp, nil
}

View File

@ -42,12 +42,13 @@ func (k *KnowledgeArticle) Validate() error {
} }
type TableOfContent struct { type TableOfContent struct {
UUID string UUID string `json:"uuid"`
ArticleID string ArticleID string `json:"article_id"`
Title string Title string `json:"title"`
Order int Level int `json:"level"`
CreatedAt time.Time Order int `json:"order"`
UpdatedAt time.Time CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type RelatedArticle struct { type RelatedArticle struct {
@ -60,13 +61,18 @@ type RelatedArticle struct {
} }
type KnowledgeHubRepository interface { type KnowledgeHubRepository interface {
// Article methods
GetArticleByID(context.Context, string) (KnowledgeArticle, error) GetArticleByID(context.Context, string) (KnowledgeArticle, error)
GetArticlesByCategory(ctx context.Context, category string) ([]KnowledgeArticle, error) GetArticlesByCategory(ctx context.Context, category string) ([]KnowledgeArticle, error)
GetAllArticles(ctx context.Context) ([]KnowledgeArticle, error) GetAllArticles(ctx context.Context) ([]KnowledgeArticle, error)
CreateOrUpdateArticle(context.Context, *KnowledgeArticle) error CreateOrUpdateArticle(context.Context, *KnowledgeArticle) error
DeleteArticle(context.Context, string) error DeleteArticle(context.Context, string) error
// Table of Contents methods
GetTableOfContents(ctx context.Context, articleID string) ([]TableOfContent, error) GetTableOfContents(ctx context.Context, articleID string) ([]TableOfContent, error)
CreateTableOfContents(ctx context.Context, articleID string, items []TableOfContent) error
// Related Articles methods
GetRelatedArticles(ctx context.Context, articleID string) ([]RelatedArticle, error) GetRelatedArticles(ctx context.Context, articleID string) ([]RelatedArticle, error)
CreateRelatedArticle(ctx context.Context, articleID string, related *RelatedArticle) error CreateRelatedArticle(ctx context.Context, articleID string, related *RelatedArticle) error
} }

View File

@ -11,4 +11,5 @@ type Connection interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error) Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row QueryRow(context.Context, string, ...interface{}) pgx.Row
BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error)
} }

View File

@ -2,6 +2,8 @@ package repository
import ( import (
"context" "context"
"fmt"
"github.com/jackc/pgx/v5"
"strings" "strings"
"github.com/forfarm/backend/internal/domain" "github.com/forfarm/backend/internal/domain"
@ -213,3 +215,51 @@ func (p *postgresKnowledgeHubRepository) CreateRelatedArticle(
) )
return err return err
} }
func (p *postgresKnowledgeHubRepository) CreateTableOfContents(
ctx context.Context,
articleID string,
items []domain.TableOfContent,
) error {
// Begin transaction
tx, err := p.conn.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer func() {
if err != nil {
tx.Rollback(ctx)
}
}()
// First delete existing TOC items for this article
_, err = tx.Exec(ctx, `DELETE FROM table_of_contents WHERE article_id = $1`, articleID)
if err != nil {
return fmt.Errorf("failed to delete existing TOC items: %w", err)
}
// Insert new TOC items
for _, item := range items {
item.UUID = uuid.New().String()
_, err = tx.Exec(ctx, `
INSERT INTO table_of_contents
(uuid, article_id, title, level, "order", created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())`,
item.UUID,
articleID,
item.Title,
item.Level,
item.Order,
)
if err != nil {
return fmt.Errorf("failed to insert TOC item: %w", err)
}
}
// Commit transaction
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}

View File

@ -0,0 +1,37 @@
package service
import (
"github.com/forfarm/backend/internal/domain"
"regexp"
"strings"
)
type TOCGenerator struct{}
func NewTOCGenerator() *TOCGenerator {
return &TOCGenerator{}
}
func (g *TOCGenerator) GenerateFromContent(content string) []domain.TableOfContent {
var toc []domain.TableOfContent
lines := strings.Split(content, "\n")
order := 0
headerRegex := regexp.MustCompile(`^(#{1,6})\s+(.*)$`)
for _, line := range lines {
if matches := headerRegex.FindStringSubmatch(line); matches != nil {
order++
level := len(matches[1]) // Number of # indicates level
title := matches[2]
toc = append(toc, domain.TableOfContent{
Title: title,
Level: level,
Order: order,
})
}
}
return toc
}

View File

@ -13,13 +13,13 @@ CREATE TABLE knowledge_articles (
); );
CREATE TABLE table_of_contents ( CREATE TABLE table_of_contents (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(), uuid UUID PRIMARY KEY,
article_id UUID NOT NULL, article_id UUID NOT NULL REFERENCES knowledge_articles(uuid),
title TEXT NOT NULL, title TEXT NOT NULL,
"order" INT NOT NULL, level INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), "order" INT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_toc_article FOREIGN KEY (article_id) REFERENCES knowledge_articles(uuid) ON DELETE CASCADE updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
CREATE TABLE related_articles ( CREATE TABLE related_articles (