Preserve publish record history
This commit is contained in:
@@ -45,6 +45,7 @@ type PublishRecordResponse struct {
|
||||
BrandDeleted bool `json:"brand_deleted"`
|
||||
ArticleTitle *string `json:"article_title,omitempty"`
|
||||
PlatformAccountID *int64 `json:"platform_account_id"`
|
||||
PlatformUID *string `json:"platform_uid,omitempty"`
|
||||
DesktopAccountID *string `json:"desktop_account_id"`
|
||||
DesktopTaskID *string `json:"desktop_task_id,omitempty"`
|
||||
TargetType string `json:"target_type"`
|
||||
@@ -81,6 +82,13 @@ type DeletePublishRecordResponse struct {
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
const visiblePublishRecordWhereSQL = `
|
||||
AND (
|
||||
pr.deleted_at IS NULL
|
||||
OR pr.status IN ('success', 'published', 'publish_success', 'failed', 'publish_failed', 'cancelled', 'canceled')
|
||||
)
|
||||
`
|
||||
|
||||
func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformResponse, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, platform_id, name, category, short_name, accent_color, login_url, logo_url, status, sort_order
|
||||
@@ -191,7 +199,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
||||
SELECT pr.id, pr.publish_batch_id, pr.article_id,
|
||||
a.brand_id, b.name AS brand_name, (b.id IS NULL OR b.deleted_at IS NOT NULL OR b.status IN ('deleting', 'deleted')) AS brand_deleted,
|
||||
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', '')) AS article_title,
|
||||
pr.platform_account_id, pa.desktop_id::text, dt.desktop_id::text,
|
||||
pr.platform_account_id, pa.platform_uid, pa.desktop_id::text, dt.desktop_id::text,
|
||||
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
||||
COALESCE(mp.name, esc.site_name, esc.name, pr.platform_id) AS platform_name,
|
||||
CASE
|
||||
@@ -227,7 +235,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
||||
AND pb.tenant_id = pr.tenant_id
|
||||
AND pb.initiator_user_id = $2
|
||||
)
|
||||
AND pr.deleted_at IS NULL
|
||||
` + visiblePublishRecordWhereSQL + `
|
||||
AND pr.status = ANY($3)
|
||||
AND (
|
||||
$4 = ''
|
||||
@@ -264,6 +272,7 @@ func (s *MediaService) listPublishRecordsByStatuses(
|
||||
&item.BrandDeleted,
|
||||
&item.ArticleTitle,
|
||||
&item.PlatformAccountID,
|
||||
&item.PlatformUID,
|
||||
&desktopAccountID,
|
||||
&desktopTaskID,
|
||||
&item.TargetType,
|
||||
@@ -318,7 +327,10 @@ func (s *MediaService) countPublishRecordsByStatuses(
|
||||
AND pb.tenant_id = pr.tenant_id
|
||||
AND pb.initiator_user_id = $2
|
||||
)
|
||||
AND pr.deleted_at IS NULL
|
||||
AND (
|
||||
pr.deleted_at IS NULL
|
||||
OR pr.status IN ('success', 'published', 'publish_success', 'failed', 'publish_failed', 'cancelled', 'canceled')
|
||||
)
|
||||
AND pr.status = ANY($3)
|
||||
AND (
|
||||
$4 = ''
|
||||
@@ -344,7 +356,7 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT pr.id, pr.publish_batch_id, pr.article_id,
|
||||
a.brand_id, b.name AS brand_name, (b.id IS NULL OR b.deleted_at IS NOT NULL OR b.status IN ('deleting', 'deleted')) AS brand_deleted,
|
||||
pr.platform_account_id, pa.desktop_id::text,
|
||||
pr.platform_account_id, pa.platform_uid, pa.desktop_id::text,
|
||||
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
||||
COALESCE(mp.name, esc.site_name, esc.name, pr.platform_id) AS platform_name,
|
||||
CASE
|
||||
@@ -362,7 +374,14 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
||||
AND esc.tenant_id = pr.tenant_id
|
||||
JOIN articles a ON a.id = pr.article_id AND a.tenant_id = pr.tenant_id
|
||||
LEFT JOIN brands b ON b.id = a.brand_id AND b.tenant_id = a.tenant_id
|
||||
WHERE pr.tenant_id = $1 AND pr.article_id = $2 AND a.brand_id = $3 AND a.deleted_at IS NULL AND pr.deleted_at IS NULL
|
||||
WHERE pr.tenant_id = $1
|
||||
AND pr.article_id = $2
|
||||
AND a.brand_id = $3
|
||||
AND a.deleted_at IS NULL
|
||||
AND (
|
||||
pr.deleted_at IS NULL
|
||||
OR pr.status IN ('success', 'published', 'publish_success', 'failed', 'publish_failed', 'cancelled', 'canceled')
|
||||
)
|
||||
ORDER BY pr.created_at DESC, pr.id DESC
|
||||
`, tenantID, articleID, brandID)
|
||||
if err != nil {
|
||||
@@ -382,6 +401,7 @@ func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID,
|
||||
&item.BrandName,
|
||||
&item.BrandDeleted,
|
||||
&item.PlatformAccountID,
|
||||
&item.PlatformUID,
|
||||
&desktopAccountID,
|
||||
&item.TargetType,
|
||||
&item.TargetConnectionID,
|
||||
@@ -456,6 +476,10 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
return nil, response.ErrInternal(50044, "publish_record_delete_lookup_failed", "failed to delete publish record")
|
||||
}
|
||||
|
||||
if !publishRecordMayBeDiscarded(publishStatus) {
|
||||
return nil, response.ErrConflict(40996, "publish_record_history_locked", "completed publish records are kept for retry and audit")
|
||||
}
|
||||
|
||||
if tag, err := tx.Exec(ctx, `
|
||||
UPDATE publish_records
|
||||
SET deleted_at = NOW(),
|
||||
@@ -469,7 +493,7 @@ func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64)
|
||||
return nil, response.ErrNotFound(40435, "publish_record_not_found", "publish record not found")
|
||||
}
|
||||
|
||||
if publishRecordMayHaveQueuedTask(publishStatus) {
|
||||
if publishRecordMayBeDiscarded(publishStatus) {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE desktop_tasks
|
||||
SET status = 'aborted',
|
||||
@@ -535,9 +559,9 @@ func publishPlatformRequiresCover(platformID string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func publishRecordMayHaveQueuedTask(status string) bool {
|
||||
func publishRecordMayBeDiscarded(status string) bool {
|
||||
switch strings.TrimSpace(status) {
|
||||
case "queued", "publishing", "pending", "running":
|
||||
case "queued", "pending":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -923,7 +923,10 @@ func (s *PublishJobService) RetryTenantDesktopTask(
|
||||
}
|
||||
|
||||
func shouldRequeuePublishTaskInsteadOfCreatingNew(task *repository.DesktopTask) bool {
|
||||
return task != nil && task.Kind == "publish" && task.Status == "unknown"
|
||||
return task != nil &&
|
||||
task.Kind == "publish" &&
|
||||
task.Status == "unknown" &&
|
||||
!publishUnknownTaskHasDefinitiveFailure(task)
|
||||
}
|
||||
|
||||
func (s *PublishJobService) requeueUnknownPublishTask(
|
||||
|
||||
@@ -161,8 +161,16 @@ func TestShouldRequeuePublishTaskInsteadOfCreatingNewOnlyForUnknownPublish(t *te
|
||||
if !shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
Status: "unknown",
|
||||
Error: []byte(`{"publish_submit_uncertain":true,"message":"network interrupted after submit"}`),
|
||||
}) {
|
||||
t.Fatal("unknown publish tasks should be retried by requeueing the existing task")
|
||||
t.Fatal("submit-uncertain unknown publish tasks should be retried by requeueing the existing task")
|
||||
}
|
||||
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
Status: "unknown",
|
||||
Error: []byte(`{"error_code":403,"error_msg":"CSRF token invalid"}`),
|
||||
}) {
|
||||
t.Fatal("definitive failed unknown publish tasks should create a new retry record")
|
||||
}
|
||||
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -193,6 +194,33 @@ func TestDesktopTaskToPublishStatusMapsAbortToCancelled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishRecordMayBeDiscardedOnlyForQueuedRecords(t *testing.T) {
|
||||
for _, status := range []string{"queued", "pending"} {
|
||||
if !publishRecordMayBeDiscarded(status) {
|
||||
t.Fatalf("publishRecordMayBeDiscarded(%q) = false, want true", status)
|
||||
}
|
||||
}
|
||||
|
||||
for _, status := range []string{"publishing", "running", "failed", "success", "cancelled"} {
|
||||
if publishRecordMayBeDiscarded(status) {
|
||||
t.Fatalf("publishRecordMayBeDiscarded(%q) = true, want false", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisiblePublishRecordWhereIncludesDeletedTerminalFailures(t *testing.T) {
|
||||
normalized := normalizeSQLWhitespace(visiblePublishRecordWhereSQL)
|
||||
if !strings.Contains(normalized, "pr.deleted_at IS NULL") {
|
||||
t.Fatalf("visible publish record filter must include active records: %s", normalized)
|
||||
}
|
||||
if !strings.Contains(normalized, "'failed'") || !strings.Contains(normalized, "'publish_failed'") {
|
||||
t.Fatalf("visible publish record filter must keep terminal failed history: %s", normalized)
|
||||
}
|
||||
if strings.Contains(normalized, "'queued'") || strings.Contains(normalized, "'publishing'") {
|
||||
t.Fatalf("visible publish record filter must not resurrect deleted active records: %s", normalized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecalculateArticlePublishStatusDefaultsToUnpublished(t *testing.T) {
|
||||
tx := &capturePublishDedupLookupTx{rows: emptyPublishDedupRows{}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user