diff --git a/server/go.mod b/server/go.mod index 4e9307f..6be294d 100644 --- a/server/go.mod +++ b/server/go.mod @@ -62,6 +62,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect @@ -84,6 +85,6 @@ require ( google.golang.org/grpc v1.66.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/go.sum b/server/go.sum index ade75d9..936accc 100644 --- a/server/go.sum +++ b/server/go.sum @@ -103,6 +103,7 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -155,6 +156,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -169,6 +172,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -277,6 +281,8 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/internal/tenant/app/article_service.go b/server/internal/tenant/app/article_service.go index 9a4df28..07037cd 100644 --- a/server/internal/tenant/app/article_service.go +++ b/server/internal/tenant/app/article_service.go @@ -21,6 +21,7 @@ import ( "github.com/geo-platform/tenant-api/internal/shared/middleware" "github.com/geo-platform/tenant-api/internal/shared/objectstorage" "github.com/geo-platform/tenant-api/internal/shared/response" + "github.com/geo-platform/tenant-api/internal/tenant/repository" ) type ArticleService struct { @@ -161,7 +162,7 @@ func (s *ArticleService) List(ctx context.Context, params ArticleListParams) (*A // List listQuery := `SELECT a.id, a.source_type, a.template_id, a.prompt_rule_id, a.generate_status, a.publish_status, a.created_at, - COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), gt.error_message, av.markdown_content, COALESCE(av.word_count, 0), av.source_label, t.template_name, pr.name, a.wizard_state_json, gt.input_params_json + COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), gt.error_message, COALESCE(av.word_count, 0), av.source_label, t.template_name, pr.name, a.wizard_state_json, gt.input_params_json FROM articles a LEFT JOIN article_versions av ON av.id = a.current_version_id LEFT JOIN article_templates t ON t.id = a.template_id @@ -236,17 +237,15 @@ func (s *ArticleService) List(ctx context.Context, params ArticleListParams) (*A for rows.Next() { var item ArticleListItem var dbSourceType string - var markdownContent *string var wizardStateJSON []byte var inputParamsJSON []byte if err := rows.Scan(&item.ID, &dbSourceType, &item.TemplateID, &item.PromptRuleID, &item.GenerateStatus, &item.PublishStatus, &item.CreatedAt, - &item.Title, &item.GenerationErrorMessage, &markdownContent, &item.WordCount, &item.SourceLabel, &item.TemplateName, &item.PromptRuleName, &wizardStateJSON, &inputParamsJSON); err != nil { + &item.Title, &item.GenerationErrorMessage, &item.WordCount, &item.SourceLabel, &item.TemplateName, &item.PromptRuleName, &wizardStateJSON, &inputParamsJSON); err != nil { return nil, response.ErrInternal(50010, "scan_failed", err.Error()) } item.SourceType = dbSourceType item.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON) item.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON) - item.WordCount = resolveWordCount(markdownContent, item.WordCount) items = append(items, item) } if items == nil { @@ -281,12 +280,13 @@ func (s *ArticleService) Detail(ctx context.Context, id int64) (*ArticleDetailRe actor := auth.MustActor(ctx) var d ArticleDetailResponse var dbSourceType string + var currentVersionID *int64 var wizardStateJSON []byte var inputParamsJSON []byte err := s.pool.QueryRow(ctx, ` - SELECT a.id, a.source_type, a.template_id, a.generate_status, a.publish_status, a.created_at, - COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), av.html_content, av.markdown_content, COALESCE(av.word_count, 0), av.source_label, av.version_no, + SELECT a.id, a.source_type, a.template_id, a.current_version_id, a.generate_status, a.publish_status, a.created_at, + COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), NULLIF(gt.input_params_json ->> 'title', '')), COALESCE(av.word_count, 0), av.source_label, av.version_no, t.template_name, gt.error_message, a.wizard_state_json, gt.input_params_json FROM articles a LEFT JOIN article_versions av ON av.id = a.current_version_id @@ -299,12 +299,20 @@ func (s *ArticleService) Detail(ctx context.Context, id int64) (*ArticleDetailRe LIMIT 1 ) gt ON true WHERE a.id = $1 AND a.tenant_id = $2 AND a.deleted_at IS NULL - `, id, actor.TenantID).Scan(&d.ID, &dbSourceType, &d.TemplateID, &d.GenerateStatus, &d.PublishStatus, &d.CreatedAt, - &d.Title, &d.HTMLContent, &d.MarkdownContent, &d.WordCount, &d.SourceLabel, &d.VersionNo, &d.TemplateName, &d.GenerationErrorMessage, &wizardStateJSON, &inputParamsJSON) + `, id, actor.TenantID).Scan(&d.ID, &dbSourceType, &d.TemplateID, ¤tVersionID, &d.GenerateStatus, &d.PublishStatus, &d.CreatedAt, + &d.Title, &d.WordCount, &d.SourceLabel, &d.VersionNo, &d.TemplateName, &d.GenerationErrorMessage, &wizardStateJSON, &inputParamsJSON) if err != nil { return nil, response.ErrNotFound(40411, "article_not_found", "article not found") } d.SourceType = dbSourceType + if currentVersionID != nil { + content, err := repository.LoadArticleVersionContent(ctx, s.pool, d.ID, *currentVersionID) + if err != nil { + return nil, response.ErrInternal(50011, "article_version_reconstruct_failed", "failed to reconstruct article version") + } + d.HTMLContent = content.HTMLContent + d.MarkdownContent = content.MarkdownContent + } if len(wizardStateJSON) > 0 { d.WizardState = map[string]interface{}{} _ = json.Unmarshal(wizardStateJSON, &d.WizardState) @@ -341,12 +349,17 @@ func (s *ArticleService) Create(ctx context.Context, req CreateArticleRequest) ( } sourceLabel := "自由创作" - var versionID int64 - if err := tx.QueryRow(ctx, ` - INSERT INTO article_versions (article_id, version_no, title, html_content, markdown_content, word_count, source_label) - VALUES ($1, 1, $2, NULL, '', 0, $3) - RETURNING id - `, articleID, title, sourceLabel).Scan(&versionID); err != nil { + articleRepo := repository.NewArticleRepository(tx) + versionID, err := articleRepo.CreateArticleVersion(ctx, repository.CreateArticleVersionInput{ + ArticleID: articleID, + VersionNo: 1, + Title: title, + HTMLContent: "", + MarkdownContent: "", + WordCount: 0, + SourceLabel: sourceLabel, + }) + if err != nil { return nil, response.ErrInternal(50014, "create_failed", "failed to create article version") } @@ -426,12 +439,17 @@ func (s *ArticleService) Update(ctx context.Context, id int64, req UpdateArticle return nil, response.ErrInternal(50012, "update_failed", "failed to update article metadata") } - var versionID int64 - if err := tx.QueryRow(ctx, ` - INSERT INTO article_versions (article_id, version_no, title, html_content, markdown_content, word_count, source_label) - VALUES ($1, $2, $3, NULL, $4, $5, $6) - RETURNING id - `, id, nextVersionNo, title, markdown, wordCount, sourceLabel).Scan(&versionID); err != nil { + articleRepo := repository.NewArticleRepository(tx) + versionID, err := articleRepo.CreateArticleVersion(ctx, repository.CreateArticleVersionInput{ + ArticleID: id, + VersionNo: nextVersionNo, + Title: title, + HTMLContent: "", + MarkdownContent: markdown, + WordCount: wordCount, + SourceLabel: sourceLabel, + }) + if err != nil { return nil, response.ErrInternal(50012, "update_failed", "failed to create article version") } @@ -527,7 +545,7 @@ func (s *ArticleService) Versions(ctx context.Context, articleID int64) ([]Versi } rows, err := s.pool.Query(ctx, ` - SELECT id, version_no, title, markdown_content, word_count, source_label, created_at + SELECT id, version_no, title, word_count, source_label, created_at FROM article_versions WHERE article_id = $1 ORDER BY version_no DESC `, articleID) if err != nil { @@ -538,11 +556,9 @@ func (s *ArticleService) Versions(ctx context.Context, articleID int64) ([]Versi var versions []VersionItem for rows.Next() { var v VersionItem - var markdownContent *string - if err := rows.Scan(&v.ID, &v.VersionNo, &v.Title, &markdownContent, &v.WordCount, &v.SourceLabel, &v.CreatedAt); err != nil { + if err := rows.Scan(&v.ID, &v.VersionNo, &v.Title, &v.WordCount, &v.SourceLabel, &v.CreatedAt); err != nil { return nil, response.ErrInternal(50010, "scan_failed", err.Error()) } - v.WordCount = resolveWordCount(markdownContent, v.WordCount) versions = append(versions, v) } if versions == nil { diff --git a/server/internal/tenant/repository/article_repo.go b/server/internal/tenant/repository/article_repo.go index 11bcbc8..c4167a2 100644 --- a/server/internal/tenant/repository/article_repo.go +++ b/server/internal/tenant/repository/article_repo.go @@ -30,11 +30,12 @@ type ArticleRepository interface { } type articleRepository struct { - q generated.Querier + db generated.DBTX + q generated.Querier } func NewArticleRepository(db generated.DBTX) ArticleRepository { - return &articleRepository{q: newQuerier(db)} + return &articleRepository{db: db, q: newQuerier(db)} } func (r *articleRepository) CreateArticle(ctx context.Context, input CreateArticleInput) (int64, error) { @@ -46,15 +47,42 @@ func (r *articleRepository) CreateArticle(ctx context.Context, input CreateArtic } func (r *articleRepository) CreateArticleVersion(ctx context.Context, input CreateArticleVersionInput) (int64, error) { - return r.q.CreateArticleVersion(ctx, generated.CreateArticleVersionParams{ - ArticleID: input.ArticleID, - VersionNo: int32(input.VersionNo), - Title: pgText(&input.Title), - HtmlContent: pgText(&input.HTMLContent), - MarkdownContent: pgText(&input.MarkdownContent), - WordCount: int32(input.WordCount), - SourceLabel: pgText(&input.SourceLabel), + 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 + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id + `, + input.ArticleID, + input.VersionNo, + pgText(&input.Title), + pgText(storedHTML), + pgText(storedMarkdown), + input.WordCount, + pgText(&input.SourceLabel), + ).Scan(&versionID) + if err != nil { + return 0, err + } + return versionID, nil } func (r *articleRepository) UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error { diff --git a/server/internal/tenant/repository/article_version_storage.go b/server/internal/tenant/repository/article_version_storage.go new file mode 100644 index 0000000..21bf122 --- /dev/null +++ b/server/internal/tenant/repository/article_version_storage.go @@ -0,0 +1,163 @@ +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 +} diff --git a/server/internal/tenant/repository/article_version_storage_test.go b/server/internal/tenant/repository/article_version_storage_test.go new file mode 100644 index 0000000..3c471f9 --- /dev/null +++ b/server/internal/tenant/repository/article_version_storage_test.go @@ -0,0 +1,67 @@ +package repository + +import "testing" + +func TestBuildArticleVersionDiffStorage_ReconstructsFromEmptyBase(t *testing.T) { + current := ArticleVersionContent{ + HTMLContent: stringPtr(""), + MarkdownContent: stringPtr("# First version\n\nHello world"), + } + + storedHTML, storedMarkdown := buildArticleVersionDiffStorage(ArticleVersionContent{}, current) + + reconstructed, err := reconstructStoredArticleVersionContents([]storedArticleVersionContent{ + { + VersionNo: 1, + HTMLContent: storedHTML, + MarkdownContent: storedMarkdown, + }, + }) + if err != nil { + t.Fatalf("reconstruct stored content: %v", err) + } + + if textValue(reconstructed.HTMLContent) != textValue(current.HTMLContent) { + t.Fatalf("unexpected html content: %q", textValue(reconstructed.HTMLContent)) + } + if textValue(reconstructed.MarkdownContent) != textValue(current.MarkdownContent) { + t.Fatalf("unexpected markdown content: %q", textValue(reconstructed.MarkdownContent)) + } +} + +func TestReconstructStoredArticleVersionContents_DiffChain(t *testing.T) { + _, markdownV1 := buildArticleVersionDiffStorage( + ArticleVersionContent{}, + ArticleVersionContent{MarkdownContent: stringPtr("alpha")}, + ) + _, markdownV2 := buildArticleVersionDiffStorage( + ArticleVersionContent{MarkdownContent: stringPtr("alpha")}, + ArticleVersionContent{MarkdownContent: stringPtr("alpha beta")}, + ) + _, markdownV3 := buildArticleVersionDiffStorage( + ArticleVersionContent{MarkdownContent: stringPtr("alpha beta")}, + ArticleVersionContent{MarkdownContent: stringPtr("alpha beta gamma")}, + ) + + reconstructed, err := reconstructStoredArticleVersionContents([]storedArticleVersionContent{ + { + VersionNo: 1, + MarkdownContent: markdownV1, + }, + { + VersionNo: 2, + MarkdownContent: markdownV2, + }, + { + VersionNo: 3, + MarkdownContent: markdownV3, + }, + }) + if err != nil { + t.Fatalf("reconstruct stored content: %v", err) + } + + if got := textValue(reconstructed.MarkdownContent); got != "alpha beta gamma" { + t.Fatalf("unexpected markdown content: %q", got) + } +} diff --git a/server/internal/tenant/repository/workspace_repo.go b/server/internal/tenant/repository/workspace_repo.go index d053e05..96bd28a 100644 --- a/server/internal/tenant/repository/workspace_repo.go +++ b/server/internal/tenant/repository/workspace_repo.go @@ -4,9 +4,6 @@ import ( "context" "time" - "github.com/jackc/pgx/v5/pgtype" - - "github.com/geo-platform/tenant-api/internal/shared/contentstats" "github.com/geo-platform/tenant-api/internal/tenant/repository/generated" ) @@ -76,7 +73,7 @@ func (r *workspaceRepository) GetRecentArticles(ctx context.Context, tenantID in GenerationMode: nullableText(row.GenerationMode), CreatedAt: timeFromTimestamp(row.CreatedAt), Title: nullableText(row.Title), - WordCount: resolveWorkspaceWordCount(row.MarkdownContent, intFromInt4(row.WordCount)), + WordCount: intFromInt4(row.WordCount), SourceLabel: nullableText(row.SourceLabel), TemplateName: nullableText(row.TemplateName), }) @@ -107,12 +104,3 @@ func (r *workspaceRepository) GetActivePlanForTenant(ctx context.Context, tenant EndAt: timeFromTimestamp(row.EndAt), }, nil } - -func resolveWorkspaceWordCount(markdown pgtype.Text, stored int) int { - if markdown.Valid { - if counted := contentstats.CountWords(markdown.String); counted > 0 { - return counted - } - } - return stored -} diff --git a/server/migrations/20260407100000_add_article_version_content_storage.down.sql b/server/migrations/20260407100000_add_article_version_content_storage.down.sql new file mode 100644 index 0000000..e0ac49d --- /dev/null +++ b/server/migrations/20260407100000_add_article_version_content_storage.down.sql @@ -0,0 +1 @@ +SELECT 1; diff --git a/server/migrations/20260407100000_add_article_version_content_storage.up.sql b/server/migrations/20260407100000_add_article_version_content_storage.up.sql new file mode 100644 index 0000000..6367679 --- /dev/null +++ b/server/migrations/20260407100000_add_article_version_content_storage.up.sql @@ -0,0 +1,6 @@ +UPDATE articles +SET current_version_id = NULL, + updated_at = NOW() +WHERE current_version_id IS NOT NULL; + +DELETE FROM article_versions;