Skip to main content

suno_core/graph/
node.rs

1//! The relational graph model: clip nodes, parent edges, and cached root
2//! resolutions. Pure serde data types with no logic of their own.
3
4use serde::{Deserialize, Serialize};
5
6use crate::lineage::{EdgeRole, ResolveStatus};
7
8/// Lifecycle marker for a [`Node`]: `"observed"` for a clip seen from the feed or gap-fill.
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum NodeStatus {
12    #[default]
13    #[serde(other)]
14    Observed,
15}
16
17/// Lifecycle marker for a [`StoredEdge`]: `"active"` for an edge observed this run.
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum EdgeStatus {
21    #[default]
22    #[serde(other)]
23    Active,
24}
25
26/// One clip in the graph. Mirrors the fields lineage needs to survive a purge:
27/// enough to name and date the clip long after Suno deletes it.
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29#[serde(default)]
30pub struct Node {
31    pub title: String,
32    pub created_at: String,
33    pub clip_type: String,
34    pub task: String,
35    pub is_remix: bool,
36    pub is_trashed: bool,
37    pub status: NodeStatus,
38    pub first_seen_at: String,
39    pub last_seen_at: String,
40}
41
42impl Default for Node {
43    fn default() -> Self {
44        Self {
45            title: String::new(),
46            created_at: String::new(),
47            clip_type: String::new(),
48            task: String::new(),
49            is_remix: false,
50            is_trashed: false,
51            status: NodeStatus::Observed,
52            first_seen_at: String::new(),
53            last_seen_at: String::new(),
54        }
55    }
56}
57
58/// One parent link, keyed (for upsert) by `(child_id, parent_id, edge_type,
59/// role, ordinal)`. A flat row, not nested under its child, so it maps directly
60/// to a `lineage_edges` table.
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62#[serde(default)]
63pub struct StoredEdge {
64    pub child_id: String,
65    pub parent_id: String,
66    /// Stable lowercase slug, e.g. `"cover"`, `"remaster"`, `"section_replace"`.
67    pub edge_type: String,
68    pub role: EdgeRole,
69    /// The clip field the parent id was read from, e.g. `"cover_clip_id"`.
70    pub source_field: String,
71    /// Position within its role (0 for the primary, then secondaries in order).
72    pub ordinal: u32,
73    pub status: EdgeStatus,
74    pub first_seen_at: String,
75    pub last_seen_at: String,
76}
77
78impl Default for StoredEdge {
79    fn default() -> Self {
80        Self {
81            child_id: String::new(),
82            parent_id: String::new(),
83            edge_type: String::new(),
84            role: EdgeRole::Primary,
85            source_field: String::new(),
86            ordinal: 0,
87            status: EdgeStatus::Active,
88            first_seen_at: String::new(),
89            last_seen_at: String::new(),
90        }
91    }
92}
93
94/// A cached root resolution for one clip: the O(1) album lookup, kept monotonic.
95#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
96#[serde(default)]
97pub struct CacheEntry {
98    pub root_id: String,
99    pub status: ResolveStatus,
100    pub algorithm_version: u32,
101    pub computed_at: String,
102}