mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 14:04:08 +01:00
feat: Update TableOfContent Handler
This commit is contained in:
parent
5eec21a2b1
commit
1dee8e7f97
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
37
backend/internal/service/toc_generator.go
Normal file
37
backend/internal/service/toc_generator.go
Normal 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
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
level INT NOT NULL,
|
||||||
"order" INT NOT NULL,
|
"order" INT NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
CONSTRAINT fk_toc_article FOREIGN KEY (article_id) REFERENCES knowledge_articles(uuid) ON DELETE CASCADE
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE related_articles (
|
CREATE TABLE related_articles (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user