restflow_core/models/
node.rs

1use super::trigger::{AuthConfig, TriggerConfig};
2use serde::{Deserialize, Serialize};
3use ts_rs::TS;
4
5#[derive(Debug, Clone, Serialize, Deserialize, TS)]
6#[ts(export)]
7pub struct Node {
8    pub id: String,
9    pub node_type: NodeType,
10    #[ts(type = "any")]
11    pub config: serde_json::Value,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub position: Option<Position>,
14}
15
16impl Node {
17    /// Check if this node is a trigger node
18    pub fn is_trigger(&self) -> bool {
19        matches!(
20            self.node_type,
21            NodeType::ManualTrigger | NodeType::WebhookTrigger | NodeType::ScheduleTrigger
22        )
23    }
24
25    /// Extract trigger configuration from node
26    pub fn extract_trigger_config(&self) -> Option<TriggerConfig> {
27        match self.node_type {
28            NodeType::ManualTrigger => {
29                // Manual trigger is a simple webhook with auto-generated path
30                Some(TriggerConfig::Webhook {
31                    path: format!("/manual/{}", self.id),
32                    method: "POST".to_string(),
33                    auth: None,
34                })
35            }
36            NodeType::WebhookTrigger => {
37                // Extract webhook config from {"type": "WebhookTrigger", "data": {...}}
38                // Falls back to root-level config for backward compatibility
39                let data = self.config.get("data").unwrap_or(&self.config);
40
41                let path = data
42                    .get("path")
43                    .and_then(|v| v.as_str())
44                    .unwrap_or(&format!("/webhook/{}", self.id))
45                    .to_string();
46
47                let method = data
48                    .get("method")
49                    .and_then(|v| v.as_str())
50                    .unwrap_or("POST")
51                    .to_string();
52
53                // Extract auth config if present
54                let auth = data.get("auth").and_then(|auth| {
55                    let auth_type = auth.get("type")?.as_str()?;
56                    match auth_type {
57                        "api_key" => {
58                            let key = auth.get("key")?.as_str()?.to_string();
59                            let header_name = auth
60                                .get("header_name")
61                                .and_then(|v| v.as_str())
62                                .map(|s| s.to_string());
63                            Some(AuthConfig::ApiKey { key, header_name })
64                        }
65                        "basic" => {
66                            let username = auth.get("username")?.as_str()?.to_string();
67                            let password = auth.get("password")?.as_str()?.to_string();
68                            Some(AuthConfig::Basic { username, password })
69                        }
70                        _ => None,
71                    }
72                });
73
74                Some(TriggerConfig::Webhook { path, method, auth })
75            }
76            NodeType::ScheduleTrigger => {
77                // Extract schedule config from {"type": "ScheduleTrigger", "data": {...}}
78                // Falls back to root-level config for backward compatibility
79                let data = self.config.get("data").unwrap_or(&self.config);
80
81                let cron = data
82                    .get("cron")
83                    .and_then(|v| v.as_str())
84                    .unwrap_or("0 * * * *")
85                    .to_string();
86
87                let timezone = data
88                    .get("timezone")
89                    .and_then(|v| v.as_str())
90                    .map(|s| s.to_string());
91
92                let payload = data.get("payload").cloned();
93
94                Some(TriggerConfig::Schedule {
95                    cron,
96                    timezone,
97                    payload,
98                })
99            }
100            _ => None,
101        }
102    }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, TS)]
106#[ts(export)]
107pub struct Position {
108    pub x: f64,
109    pub y: f64,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
113#[ts(export)]
114pub enum NodeType {
115    ManualTrigger,
116    WebhookTrigger,
117    ScheduleTrigger,
118    Agent,
119    HttpRequest,
120    Print,
121    DataTransform,
122    Python,
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::models::trigger::{AuthConfig, TriggerConfig};
129    use serde_json::json;
130
131    #[test]
132    fn test_extract_webhook_trigger_config() {
133        // Test with new format (config wrapped in {"type": "...", "data": {...}})
134        let node = Node {
135            id: "webhook-1".to_string(),
136            node_type: NodeType::WebhookTrigger,
137            config: json!({
138                "type": "WebhookTrigger",
139                "data": {
140                    "path": "/webhook/test",
141                    "method": "POST"
142                }
143            }),
144            position: None,
145        };
146
147        let config = node.extract_trigger_config();
148        assert!(config.is_some());
149
150        if let Some(TriggerConfig::Webhook { path, method, auth }) = config {
151            assert_eq!(path, "/webhook/test");
152            assert_eq!(method, "POST");
153            assert!(auth.is_none());
154        } else {
155            panic!("Expected Webhook trigger config");
156        }
157    }
158
159    #[test]
160    fn test_extract_webhook_trigger_config_with_auth() {
161        let node = Node {
162            id: "webhook-2".to_string(),
163            node_type: NodeType::WebhookTrigger,
164            config: json!({
165                "type": "WebhookTrigger",
166                "data": {
167                    "path": "/api/webhook",
168                    "method": "PUT",
169                    "auth": {
170                        "type": "api_key",
171                        "key": "secret123",
172                        "header_name": "X-API-Key"
173                    }
174                }
175            }),
176            position: None,
177        };
178
179        let config = node.extract_trigger_config();
180        assert!(config.is_some());
181
182        if let Some(TriggerConfig::Webhook { path, method, auth }) = config {
183            assert_eq!(path, "/api/webhook");
184            assert_eq!(method, "PUT");
185            assert!(auth.is_some());
186
187            if let Some(AuthConfig::ApiKey { key, header_name }) = auth {
188                assert_eq!(key, "secret123");
189                assert_eq!(header_name, Some("X-API-Key".to_string()));
190            } else {
191                panic!("Expected ApiKey auth config");
192            }
193        } else {
194            panic!("Expected Webhook trigger config");
195        }
196    }
197
198    #[test]
199    fn test_extract_schedule_trigger_config() {
200        let node = Node {
201            id: "schedule-1".to_string(),
202            node_type: NodeType::ScheduleTrigger,
203            config: json!({
204                "type": "ScheduleTrigger",
205                "data": {
206                    "cron": "0 10 * * *",
207                    "timezone": "America/New_York",
208                    "payload": {"key": "value"}
209                }
210            }),
211            position: None,
212        };
213
214        let config = node.extract_trigger_config();
215        assert!(config.is_some());
216
217        if let Some(TriggerConfig::Schedule {
218            cron,
219            timezone,
220            payload,
221        }) = config
222        {
223            assert_eq!(cron, "0 10 * * *");
224            assert_eq!(timezone, Some("America/New_York".to_string()));
225            assert_eq!(payload, Some(json!({"key": "value"})));
226        } else {
227            panic!("Expected Schedule trigger config");
228        }
229    }
230
231    #[test]
232    fn test_extract_manual_trigger_config() {
233        let node = Node {
234            id: "manual-1".to_string(),
235            node_type: NodeType::ManualTrigger,
236            config: json!({
237                "type": "ManualTrigger",
238                "data": {
239                    "payload": null
240                }
241            }),
242            position: None,
243        };
244
245        let config = node.extract_trigger_config();
246        assert!(config.is_some());
247
248        if let Some(TriggerConfig::Webhook { path, method, auth }) = config {
249            assert_eq!(path, "/manual/manual-1");
250            assert_eq!(method, "POST");
251            assert!(auth.is_none());
252        } else {
253            panic!("Expected Webhook trigger config for ManualTrigger");
254        }
255    }
256
257    #[test]
258    fn test_backward_compatibility_webhook() {
259        // Test old format (without "type" and "data" wrapper)
260        let node = Node {
261            id: "webhook-old".to_string(),
262            node_type: NodeType::WebhookTrigger,
263            config: json!({
264                "path": "/old/webhook",
265                "method": "GET"
266            }),
267            position: None,
268        };
269
270        let config = node.extract_trigger_config();
271        assert!(config.is_some());
272
273        if let Some(TriggerConfig::Webhook { path, method, .. }) = config {
274            assert_eq!(path, "/old/webhook");
275            assert_eq!(method, "GET");
276        } else {
277            panic!("Expected Webhook trigger config");
278        }
279    }
280
281    #[test]
282    fn test_backward_compatibility_schedule() {
283        // Test old format (without "type" and "data" wrapper)
284        let node = Node {
285            id: "schedule-old".to_string(),
286            node_type: NodeType::ScheduleTrigger,
287            config: json!({
288                "cron": "0 0 * * *",
289                "timezone": "UTC"
290            }),
291            position: None,
292        };
293
294        let config = node.extract_trigger_config();
295        assert!(config.is_some());
296
297        if let Some(TriggerConfig::Schedule { cron, timezone, .. }) = config {
298            assert_eq!(cron, "0 0 * * *");
299            assert_eq!(timezone, Some("UTC".to_string()));
300        } else {
301            panic!("Expected Schedule trigger config");
302        }
303    }
304}