490c6c759d
Replace the freeform `platforms` field on articles/tasks (parsed out of wizard_state/input_params JSON) with `auto_publish_platforms`, resolved by joining schedule publish accounts to platform_accounts. The migration strips the legacy keys from existing rows so the new field is the single source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
213 lines
4.6 KiB
Go
213 lines
4.6 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
func normalizePlatformIDs(platformIDs []string) []string {
|
|
if len(platformIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(platformIDs))
|
|
normalized := make([]string, 0, len(platformIDs))
|
|
for _, platformID := range platformIDs {
|
|
next := strings.TrimSpace(platformID)
|
|
if next == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[next]; exists {
|
|
continue
|
|
}
|
|
seen[next] = struct{}{}
|
|
normalized = append(normalized, next)
|
|
}
|
|
|
|
if len(normalized) == 0 {
|
|
return nil
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
type scheduleAutoPublishPlatformQuerier interface {
|
|
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
|
}
|
|
|
|
func loadScheduleAutoPublishPlatforms(
|
|
ctx context.Context,
|
|
db scheduleAutoPublishPlatformQuerier,
|
|
tenantID int64,
|
|
inputParamsJSON []byte,
|
|
) ([]string, error) {
|
|
if db == nil || tenantID <= 0 || len(inputParamsJSON) == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(inputParamsJSON, &payload); err != nil {
|
|
return []string{}, nil
|
|
}
|
|
if !extractBool(payload, "schedule_auto_publish") {
|
|
return []string{}, nil
|
|
}
|
|
|
|
accountIDs := normalizeSchedulePublishAccountIDs(extractStringList(payload["schedule_publish_account_ids"], 64))
|
|
if len(accountIDs) == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
return loadPublishAccountPlatformsByIDs(ctx, db, tenantID, accountIDs)
|
|
}
|
|
|
|
func loadPublishAccountPlatformsByIDs(
|
|
ctx context.Context,
|
|
db scheduleAutoPublishPlatformQuerier,
|
|
tenantID int64,
|
|
accountIDs []string,
|
|
) ([]string, error) {
|
|
accountIDs = normalizeSchedulePublishAccountIDs(accountIDs)
|
|
if db == nil || tenantID <= 0 || len(accountIDs) == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
rows, err := db.Query(ctx, `
|
|
SELECT DISTINCT platform_id
|
|
FROM platform_accounts
|
|
WHERE tenant_id = $1
|
|
AND desktop_id::text = ANY($2::text[])
|
|
AND deleted_at IS NULL
|
|
ORDER BY platform_id
|
|
`, tenantID, accountIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
platformIDs := make([]string, 0, len(accountIDs))
|
|
for rows.Next() {
|
|
var platformID string
|
|
if err := rows.Scan(&platformID); err != nil {
|
|
return nil, err
|
|
}
|
|
platformIDs = append(platformIDs, platformID)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return emptyStringSliceIfNil(normalizePlatformIDs(platformIDs)), nil
|
|
}
|
|
|
|
func emptyStringSliceIfNil(values []string) []string {
|
|
if values == nil {
|
|
return []string{}
|
|
}
|
|
return values
|
|
}
|
|
|
|
func resolveArticleCoverAssetURL(wizardStateJSON []byte) *string {
|
|
value := strings.TrimSpace(extractStringFromJSONPayload(wizardStateJSON, "cover_asset_url"))
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func resolveArticleCoverImageAssetID(wizardStateJSON []byte) *int64 {
|
|
value, ok := extractInt64FromJSONPayload(wizardStateJSON, "cover_image_asset_id")
|
|
if !ok || value <= 0 {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func extractStringFromJSONPayload(raw []byte, key string) string {
|
|
if len(raw) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
return ""
|
|
}
|
|
|
|
value, ok := payload[key].(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func extractInt64FromJSONPayload(raw []byte, key string) (int64, bool) {
|
|
if len(raw) == 0 {
|
|
return 0, false
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
return 0, false
|
|
}
|
|
|
|
switch value := payload[key].(type) {
|
|
case nil:
|
|
return 0, false
|
|
case float64:
|
|
return int64(value), true
|
|
case int64:
|
|
return value, true
|
|
case int:
|
|
return int64(value), true
|
|
case string:
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return 0, false
|
|
}
|
|
parsed, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return parsed, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func mergeArticleWizardState(raw []byte, title string, coverAssetURL *string, coverImageAssetID NullableInt64Input) ([]byte, error) {
|
|
state := make(map[string]interface{})
|
|
if len(raw) > 0 {
|
|
_ = json.Unmarshal(raw, &state)
|
|
}
|
|
|
|
delete(state, "platforms")
|
|
delete(state, "target_platforms")
|
|
delete(state, "target_platform")
|
|
|
|
trimmedTitle := strings.TrimSpace(title)
|
|
if trimmedTitle != "" {
|
|
state["title"] = trimmedTitle
|
|
}
|
|
|
|
if coverAssetURL != nil {
|
|
trimmedCoverURL := strings.TrimSpace(*coverAssetURL)
|
|
if trimmedCoverURL == "" {
|
|
delete(state, "cover_asset_url")
|
|
} else {
|
|
state["cover_asset_url"] = trimmedCoverURL
|
|
}
|
|
}
|
|
|
|
if coverImageAssetID.Set {
|
|
if coverImageAssetID.Value == nil {
|
|
delete(state, "cover_image_asset_id")
|
|
} else {
|
|
state["cover_image_asset_id"] = *coverImageAssetID.Value
|
|
}
|
|
}
|
|
|
|
return json.Marshal(state)
|
|
}
|