Files
geo/server/internal/tenant/repository/article_repo.go
T

202 lines
4.9 KiB
Go

package repository
import (
"context"
"strings"
"github.com/geo-platform/tenant-api/internal/shared/contentstats"
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
"github.com/jackc/pgx/v5"
)
type CreateArticleInput struct {
TenantID int64
SourceType string
TemplateID *int64
KolPromptID *int64
}
type CreateArticleVersionInput struct {
ArticleID int64
VersionNo int
Title string
HTMLContent string
MarkdownContent string
WordCount int
SourceLabel string
}
type ArticleRepository interface {
CreateArticle(ctx context.Context, input CreateArticleInput) (int64, error)
CreateArticleVersion(ctx context.Context, input CreateArticleVersionInput) (int64, error)
UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error
UpdateArticleGenerateStatus(ctx context.Context, articleID, tenantID int64, status string) error
}
type articleRepository struct {
db generated.DBTX
q generated.Querier
}
func NewArticleRepository(db generated.DBTX) ArticleRepository {
return &articleRepository{db: db, q: newQuerier(db)}
}
func (r *articleRepository) CreateArticle(ctx context.Context, input CreateArticleInput) (int64, error) {
return r.q.CreateArticle(ctx, generated.CreateArticleParams{
TenantID: input.TenantID,
SourceType: input.SourceType,
TemplateID: pgInt8(input.TemplateID),
KolPromptID: pgInt8(input.KolPromptID),
})
}
func NextArticleVersionNo(ctx context.Context, db generated.DBTX, articleID int64) (int, error) {
var nextVersionNo int
err := db.QueryRow(ctx, `
SELECT COALESCE(MAX(version_no), 0) + 1
FROM article_versions
WHERE article_id = $1
`, articleID).Scan(&nextVersionNo)
return nextVersionNo, err
}
func (r *articleRepository) CreateArticleVersion(ctx context.Context, input CreateArticleVersionInput) (int64, error) {
if input.VersionNo <= 0 {
nextVersionNo, err := NextArticleVersionNo(ctx, r.db, input.ArticleID)
if err != nil {
return 0, err
}
input.VersionNo = nextVersionNo
}
previous, err := loadLatestArticleVersionContent(ctx, r.db, input.ArticleID)
if err != nil {
return 0, err
}
storedHTML, storedMarkdown := buildArticleVersionDiffStorage(*previous, ArticleVersionContent{
HTMLContent: stringPtr(input.HTMLContent),
MarkdownContent: stringPtr(input.MarkdownContent),
})
var versionID int64
err = r.db.QueryRow(ctx, `
INSERT INTO article_versions (
article_id,
version_no,
title,
html_content,
markdown_content,
word_count,
source_label,
plaintext_snapshot
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id
`,
input.ArticleID,
input.VersionNo,
pgText(&input.Title),
pgText(storedHTML),
pgText(storedMarkdown),
input.WordCount,
pgText(&input.SourceLabel),
strings.TrimSpace(input.Title+"\n"+ExtractArticlePlaintext(input.MarkdownContent)),
).Scan(&versionID)
if err != nil {
return 0, err
}
return versionID, nil
}
func ExtractArticlePlaintext(markdown string) string {
markdown = strings.TrimSpace(markdown)
if markdown == "" {
return ""
}
var builder strings.Builder
builder.Grow(len(markdown))
inFence := false
lines := strings.Split(markdown, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") {
inFence = !inFence
continue
}
if inFence {
builder.WriteString(trimmed)
builder.WriteByte('\n')
continue
}
line = stripMarkdownLine(line)
if strings.TrimSpace(line) == "" {
continue
}
builder.WriteString(line)
builder.WriteByte('\n')
}
return strings.TrimSpace(builder.String())
}
func stripMarkdownLine(line string) string {
replacer := strings.NewReplacer(
"#", " ",
"*", " ",
"_", " ",
"`", " ",
">", " ",
"[", " ",
"]", " ",
"(", " ",
")", " ",
"!", " ",
"|", " ",
)
line = replacer.Replace(line)
line = strings.TrimLeft(line, "-+0123456789. \t")
return strings.Join(strings.Fields(line), " ")
}
func CountArticlePlaintextWords(markdown string) int {
return contentstats.CountWords(ExtractArticlePlaintext(markdown))
}
func (r *articleRepository) UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error {
tag, err := r.db.Exec(ctx, `
UPDATE articles
SET current_version_id = $1,
updated_at = NOW()
WHERE id = $2
AND tenant_id = $3
AND deleted_at IS NULL
`, pgInt8(&versionID), articleID, tenantID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func (r *articleRepository) UpdateArticleGenerateStatus(ctx context.Context, articleID, tenantID int64, status string) error {
tag, err := r.db.Exec(ctx, `
UPDATE articles
SET generate_status = $1,
updated_at = NOW()
WHERE id = $2
AND tenant_id = $3
AND deleted_at IS NULL
`, status, articleID, tenantID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}