Skip to main content

stmo_cli/commands/
fetch.rs

1#![allow(clippy::missing_errors_doc)]
2
3use anyhow::{Context, Result};
4use std::fs;
5use std::path::Path;
6
7use crate::api::RedashClient;
8
9fn slugify(s: &str) -> String {
10    s.to_lowercase()
11        .chars()
12        .map(|c| if c.is_alphanumeric() { c } else { '-' })
13        .collect::<String>()
14        .split('-')
15        .filter(|s| !s.is_empty())
16        .collect::<Vec<_>>()
17        .join("-")
18}
19
20fn extract_query_ids_from_directory() -> Result<Vec<u64>> {
21    let queries_dir = Path::new("queries");
22
23    if !queries_dir.exists() {
24        return Ok(Vec::new());
25    }
26
27    let mut query_ids = Vec::new();
28
29    for entry in fs::read_dir(queries_dir).context("Failed to read queries directory")? {
30        let entry = entry.context("Failed to read directory entry")?;
31        let path = entry.path();
32
33        if path.extension().is_some_and(|ext| ext == "yaml")
34            && let Some(filename) = path.file_name().and_then(|f| f.to_str())
35            && let Some(id_str) = filename.split('-').next()
36            && let Ok(id) = id_str.parse::<u64>()
37        {
38            query_ids.push(id);
39        }
40    }
41
42    query_ids.sort_unstable();
43    query_ids.dedup();
44
45    Ok(query_ids)
46}
47
48pub async fn fetch(client: &RedashClient, query_ids: Vec<u64>, all: bool) -> Result<()> {
49    fs::create_dir_all("queries")
50        .context("Failed to create queries directory")?;
51
52    let existing_query_ids = extract_query_ids_from_directory()?;
53
54    let queries_to_fetch = if all {
55        if existing_query_ids.is_empty() {
56            anyhow::bail!("No queries found in queries/ directory. Use specific query IDs or run 'discover' to see available queries.");
57        }
58        println!("Fetching {} queries from local directory...\n", existing_query_ids.len());
59        let mut queries = Vec::new();
60        for id in &existing_query_ids {
61            match client.get_query(*id).await {
62                Ok(query) => queries.push(query),
63                Err(e) => eprintln!("  ⚠ Query {id} failed to fetch: {e}"),
64            }
65        }
66        queries
67    } else if !query_ids.is_empty() {
68        println!("Fetching {} specific queries...\n", query_ids.len());
69        let mut queries = Vec::new();
70        for id in &query_ids {
71            match client.get_query(*id).await {
72                Ok(query) => queries.push(query),
73                Err(e) => eprintln!("  ⚠ Query {id} failed to fetch: {e}"),
74            }
75        }
76        queries
77    } else {
78        anyhow::bail!("No query IDs specified. Use --all to fetch tracked queries, or provide specific query IDs.\n\nExamples:\n  stmo-cli fetch --all\n  stmo-cli fetch 123 456 789\n  stmo-cli discover  (to see available queries)");
79    };
80
81    println!("Fetching {} queries...", queries_to_fetch.len());
82
83    let mut archived_queries = Vec::new();
84
85    for query in &queries_to_fetch {
86        let slug = slugify(&query.name);
87        let filename_base = format!("{}-{}", query.id, slug);
88
89        let sql_path = format!("queries/{filename_base}.sql");
90        fs::write(&sql_path, &query.sql)
91            .context(format!("Failed to write {sql_path}"))?;
92
93        let metadata = crate::models::QueryMetadata {
94            id: query.id,
95            name: query.name.clone(),
96            description: query.description.clone(),
97            data_source_id: query.data_source_id,
98            user_id: query.user.as_ref().map(|u| u.id),
99            schedule: query.schedule.clone(),
100            options: query.options.clone(),
101            visualizations: query.visualizations.clone(),
102            tags: query.tags.clone(),
103        };
104
105        let yaml_path = format!("queries/{filename_base}.yaml");
106        let yaml_content = serde_yaml::to_string(&metadata)
107            .context("Failed to serialize query metadata")?;
108        fs::write(&yaml_path, yaml_content)
109            .context(format!("Failed to write {yaml_path}"))?;
110
111        if query.is_archived {
112            archived_queries.push((query.id, query.name.clone()));
113            println!("  ✓ {} - {} [ARCHIVED]", query.id, query.name);
114        } else {
115            println!("  ✓ {} - {}", query.id, query.name);
116        }
117    }
118
119    println!("\n✓ All resources fetched successfully");
120
121    if !archived_queries.is_empty() {
122        println!("\n⚠ Warning: {} archived queries have local files:", archived_queries.len());
123        for (id, name) in &archived_queries {
124            println!("  - {id}: {name}");
125        }
126        let binary_name = std::env::args().next()
127            .and_then(|path| std::path::Path::new(&path).file_name().map(|s| s.to_string_lossy().to_string()))
128            .unwrap_or_else(|| "stmo-cli".to_string());
129        println!("\nConsider cleaning up with: {binary_name} archive --cleanup");
130    }
131
132    Ok(())
133}
134
135#[cfg(test)]
136#[allow(clippy::missing_errors_doc)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_slugify_simple() {
142        assert_eq!(slugify("Hello World"), "hello-world");
143    }
144
145    #[test]
146    fn test_slugify_special_chars() {
147        assert_eq!(slugify("Foo & Bar!"), "foo-bar");
148        assert_eq!(slugify("Test@#$%Query"), "test-query");
149    }
150
151    #[test]
152    fn test_slugify_unicode() {
153        assert_eq!(slugify("Café Münch"), "café-münch");
154        assert_eq!(slugify("日本語"), "日本語");
155    }
156
157    #[test]
158    fn test_slugify_multiple_spaces() {
159        assert_eq!(slugify("a  b   c"), "a-b-c");
160        assert_eq!(slugify("  leading and trailing  "), "leading-and-trailing");
161    }
162
163    #[test]
164    fn test_slugify_already_slugified() {
165        assert_eq!(slugify("already-slug"), "already-slug");
166        assert_eq!(slugify("some-kebab-case"), "some-kebab-case");
167    }
168
169    #[test]
170    fn test_slugify_numbers() {
171        assert_eq!(slugify("Query 123"), "query-123");
172        assert_eq!(slugify("123-456"), "123-456");
173    }
174
175    #[test]
176    fn test_slugify_mixed() {
177        assert_eq!(slugify("Mozilla's .deb Package!"), "mozilla-s-deb-package");
178        assert_eq!(slugify("Copy of 100234 - Gecko decision task"), "copy-of-100234-gecko-decision-task");
179    }
180}