turash/bugulma/backend/internal/domain/timeline.go

187 lines
6.0 KiB
Go

package domain
import (
"database/sql"
"encoding/json"
"time"
"gorm.io/datatypes"
)
// TimelineCategory defines the category of a timeline event
type TimelineCategory string
const (
TimelineCategoryPolitical TimelineCategory = "political"
TimelineCategoryMilitary TimelineCategory = "military"
TimelineCategoryEconomic TimelineCategory = "economic"
TimelineCategoryCultural TimelineCategory = "cultural"
TimelineCategorySocial TimelineCategory = "social"
TimelineCategoryNatural TimelineCategory = "natural"
TimelineCategoryInfrastructure TimelineCategory = "infrastructure"
TimelineCategoryCriminal TimelineCategory = "criminal"
)
// TimelineKind defines the kind/type of timeline event
type TimelineKind string
const (
TimelineKindHistorical TimelineKind = "historical"
TimelineKindLegend TimelineKind = "legend"
TimelineKindMixed TimelineKind = "mixed"
)
// IsValidTimelineCategory checks if a category value is valid
func IsValidTimelineCategory(category TimelineCategory) bool {
validCategories := map[TimelineCategory]bool{
TimelineCategoryPolitical: true,
TimelineCategoryMilitary: true,
TimelineCategoryEconomic: true,
TimelineCategoryCultural: true,
TimelineCategorySocial: true,
TimelineCategoryNatural: true,
TimelineCategoryInfrastructure: true,
TimelineCategoryCriminal: true,
}
return validCategories[category]
}
// IsValidTimelineKind checks if a kind value is valid
func IsValidTimelineKind(kind TimelineKind) bool {
validKinds := map[TimelineKind]bool{
TimelineKindHistorical: true,
TimelineKindLegend: true,
TimelineKindMixed: true,
}
return validKinds[kind]
}
// TimelineItem represents a historical event or period that can be displayed in various contexts
type TimelineItem struct {
ID string `gorm:"primaryKey;type:varchar(50)" json:"id"`
Title string `gorm:"type:varchar(255);not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Summary string `gorm:"type:text" json:"summary"` // Short 1-2 sentence description
ImageURL string `gorm:"type:text" json:"image_url"`
IconName string `gorm:"type:varchar(50);not null" json:"icon_name"`
Order int `gorm:"not null" json:"order"`
Heritage sql.NullBool `json:"heritage"` // Whether this item is for heritage page display (nullable to allow false)
// Time range
TimeFrom *time.Time `gorm:"type:timestamp" json:"time_from"` // Start date/time
TimeTo *time.Time `gorm:"type:timestamp" json:"time_to"` // End date/time
// Categorization
Category TimelineCategory `gorm:"type:varchar(50)" json:"category"` // political | military | economic | cultural | social | natural | infrastructure | criminal
Kind TimelineKind `gorm:"type:varchar(20)" json:"kind"` // historical | legend | mixed
IsHistorical sql.NullBool `json:"is_historical"` // Whether this is a verified historical event (nullable to allow false)
// Importance level (1-10, higher = more important)
Importance int `gorm:"default:1" json:"importance"`
// Related data as JSON arrays
// Store arrays as JSONB in Postgres. Use datatypes.JSON so GORM will
// correctly write/read JSONB columns. MarshalJSON will decode these
// into native []string when producing API responses.
Locations datatypes.JSON `gorm:"type:jsonb;default:'[]'::jsonb" json:"locations"`
Actors datatypes.JSON `gorm:"type:jsonb;default:'[]'::jsonb" json:"actors"`
Related datatypes.JSON `gorm:"type:jsonb;default:'[]'::jsonb" json:"related"`
Tags datatypes.JSON `gorm:"type:jsonb;default:'[]'::jsonb" json:"tags"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName specifies the table name for TimelineItem
func (TimelineItem) TableName() string {
return "timeline_items"
}
// GetEntityType implements Localizable interface
func (t *TimelineItem) GetEntityType() string {
return "timeline_item"
}
// GetEntityID implements Localizable interface
func (t *TimelineItem) GetEntityID() string {
return t.ID
}
// MarshalJSON implements custom JSON marshaling for TimelineItem
func (t TimelineItem) MarshalJSON() ([]byte, error) {
// Create a map for JSON serialization
result := map[string]interface{}{
"id": t.ID,
"title": t.Title,
"content": t.Content,
"summary": t.Summary,
"image_url": t.ImageURL,
"icon_name": t.IconName,
"order": t.Order,
"time_from": t.TimeFrom,
"time_to": t.TimeTo,
"category": t.Category,
"kind": t.Kind,
"importance": t.Importance,
"locations": t.Locations,
"actors": t.Actors,
"related": t.Related,
"tags": t.Tags,
"created_at": t.CreatedAt,
"updated_at": t.UpdatedAt,
}
// Handle nullable boolean fields
if t.Heritage.Valid {
result["heritage"] = t.Heritage.Bool
} else {
result["heritage"] = nil
}
if t.IsHistorical.Valid {
result["is_historical"] = t.IsHistorical.Bool
} else {
result["is_historical"] = nil
}
// Ensure stored JSON fields are valid JSON arrays. When using datatypes.JSON
// we should decode the raw bytes into Go slices for consistent API output.
// Attempt to decode locations/actors/related/tags into []string and set
// the result to the decoded slice; on failure fall back to raw JSON value.
var list []string
if len(t.Locations) > 0 {
if err := json.Unmarshal(t.Locations, &list); err == nil {
result["locations"] = list
} else {
result["locations"] = t.Locations
}
}
if len(t.Actors) > 0 {
if err := json.Unmarshal(t.Actors, &list); err == nil {
result["actors"] = list
} else {
result["actors"] = t.Actors
}
}
if len(t.Related) > 0 {
if err := json.Unmarshal(t.Related, &list); err == nil {
result["related"] = list
} else {
result["related"] = t.Related
}
}
if len(t.Tags) > 0 {
if err := json.Unmarshal(t.Tags, &list); err == nil {
result["tags"] = list
} else {
result["tags"] = t.Tags
}
}
return json.Marshal(result)
}