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

164 lines
3.9 KiB
Go

package repository
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
)
type ArticleVersionContent struct {
HTMLContent *string
MarkdownContent *string
}
type storedArticleVersionContent struct {
VersionNo int
HTMLContent *string
MarkdownContent *string
}
func LoadArticleVersionContent(
ctx context.Context,
db generated.DBTX,
articleID int64,
versionID int64,
) (*ArticleVersionContent, error) {
rows, err := db.Query(ctx, `
SELECT version_no, html_content, markdown_content
FROM article_versions
WHERE article_id = $1
AND version_no <= (
SELECT version_no
FROM article_versions
WHERE id = $2 AND article_id = $1
)
ORDER BY version_no ASC
`, articleID, versionID)
if err != nil {
return nil, err
}
defer rows.Close()
versions, err := collectStoredArticleVersionContents(rows)
if err != nil {
return nil, err
}
if len(versions) == 0 {
return nil, pgx.ErrNoRows
}
return reconstructStoredArticleVersionContents(versions)
}
func loadLatestArticleVersionContent(
ctx context.Context,
db generated.DBTX,
articleID int64,
) (*ArticleVersionContent, error) {
rows, err := db.Query(ctx, `
SELECT version_no, html_content, markdown_content
FROM article_versions
WHERE article_id = $1
ORDER BY version_no ASC
`, articleID)
if err != nil {
return nil, err
}
defer rows.Close()
versions, err := collectStoredArticleVersionContents(rows)
if err != nil {
return nil, err
}
if len(versions) == 0 {
return &ArticleVersionContent{}, nil
}
return reconstructStoredArticleVersionContents(versions)
}
func collectStoredArticleVersionContents(rows pgx.Rows) ([]storedArticleVersionContent, error) {
versions := make([]storedArticleVersionContent, 0)
for rows.Next() {
var version storedArticleVersionContent
if err := rows.Scan(&version.VersionNo, &version.HTMLContent, &version.MarkdownContent); err != nil {
return nil, err
}
versions = append(versions, version)
}
if err := rows.Err(); err != nil {
return nil, err
}
return versions, nil
}
func buildArticleVersionDiffStorage(previous, current ArticleVersionContent) (*string, *string) {
return stringPtr(articleVersionPatchText(textValue(previous.HTMLContent), textValue(current.HTMLContent))),
stringPtr(articleVersionPatchText(textValue(previous.MarkdownContent), textValue(current.MarkdownContent)))
}
func reconstructStoredArticleVersionContents(versions []storedArticleVersionContent) (*ArticleVersionContent, error) {
var html string
var markdown string
for _, version := range versions {
nextHTML, err := applyArticleVersionPatch(html, version.HTMLContent)
if err != nil {
return nil, fmt.Errorf("apply html patch for version %d: %w", version.VersionNo, err)
}
nextMarkdown, err := applyArticleVersionPatch(markdown, version.MarkdownContent)
if err != nil {
return nil, fmt.Errorf("apply markdown patch for version %d: %w", version.VersionNo, err)
}
html = nextHTML
markdown = nextMarkdown
}
result := &ArticleVersionContent{}
result.HTMLContent = stringPtr(html)
result.MarkdownContent = stringPtr(markdown)
return result, nil
}
func articleVersionPatchText(previous, current string) string {
dmp := diffmatchpatch.New()
patches := dmp.PatchMake(previous, current)
return dmp.PatchToText(patches)
}
func applyArticleVersionPatch(base string, patchText *string) (string, error) {
if patchText == nil || *patchText == "" {
return base, nil
}
dmp := diffmatchpatch.New()
patches, err := dmp.PatchFromText(*patchText)
if err != nil {
return "", err
}
next, applied := dmp.PatchApply(patches, base)
for _, ok := range applied {
if !ok {
return "", fmt.Errorf("patch apply failed")
}
}
return next, nil
}
func textValue(value *string) string {
if value == nil {
return ""
}
return *value
}
func stringPtr(value string) *string {
copied := value
return &copied
}