Files
RuDS/crates/bds-core/src/model/post.rs
2026-04-04 15:46:57 +02:00

115 lines
3.8 KiB
Rust

use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PostStatus {
Draft,
Published,
Archived,
}
impl PostStatus {
/// Returns true if this status is valid for a translation (draft or published only).
pub fn is_valid_for_translation(&self) -> bool {
matches!(self, PostStatus::Draft | PostStatus::Published)
}
}
/// A blog post. Matches the `posts` table schema.
///
/// NOTE: `content` is null for published posts — body lives in the filesystem
/// `.md` file only. Draft content is stored in DB.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Post {
pub id: String,
pub project_id: String,
pub title: String,
pub slug: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub excerpt: Option<String>,
/// Draft body text. Null/empty when published (content is in the file).
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
pub status: PostStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default)]
pub do_not_translate: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub template_slug: Option<String>,
pub file_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
/// JSON-serialized string array in DB.
#[serde(default)]
pub tags: Vec<String>,
/// JSON-serialized string array in DB.
#[serde(default)]
pub categories: Vec<String>,
// Published snapshot fields (used for diff detection)
#[serde(skip_serializing_if = "Option::is_none")]
pub published_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_tags: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_categories: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_excerpt: Option<String>,
/// Unix timestamp (integer in DB).
pub created_at: i64,
pub updated_at: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_at: Option<i64>,
}
/// A translation of a post into another language.
/// Matches the `post_translations` table.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostTranslation {
pub id: String,
pub project_id: String,
pub translation_for: String,
pub language: String,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub excerpt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
pub status: PostStatus,
pub file_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
pub created_at: i64,
pub updated_at: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub published_at: Option<i64>,
}
/// A link between two posts (tracked for backlinks).
/// Matches the `post_links` table.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostLink {
pub id: String,
pub source_post_id: String,
pub target_post_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub link_text: Option<String>,
pub created_at: i64,
}
/// Association between a post and media item.
/// Matches the `post_media` table.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostMedia {
pub id: String,
pub project_id: String,
pub post_id: String,
pub media_id: String,
pub sort_order: i32,
pub created_at: i64,
}