roam-sdk 0.4.0

Roam Research SDK and terminal UI client
Documentation
pub fn daily_note_uid_for_date(month: u32, day: u32, year: i32) -> String {
    format!("{:02}-{:02}-{}", month, day, year)
}

pub fn pull_daily_note(uid: &str) -> (serde_json::Value, String) {
    let eid = serde_json::Value::String(format!("[:block/uid \"{}\"]", uid));
    let selector = page_selector();
    (eid, selector)
}

pub fn pull_page_by_title(title: &str) -> (serde_json::Value, String) {
    let eid = serde_json::Value::String(format!("[:node/title \"{}\"]", title));
    let selector = page_selector();
    (eid, selector)
}

pub fn linked_refs_query(page_title: &str) -> String {
    let escaped = page_title.replace('"', r#"\""#);
    format!(
        r#"[:find ?uid ?s ?page-title :where [?target :node/title "{}"] [?b :block/refs ?target] [?b :block/uid ?uid] [?b :block/string ?s] [?b :block/page ?p] [?p :node/title ?page-title]]"#,
        escaped
    )
}

pub fn all_page_titles_query() -> String {
    "[:find ?title ?uid :where [?e :node/title ?title] [?e :block/uid ?uid]]".to_string()
}

pub fn search_blocks_query() -> String {
    "[:find ?uid ?s ?page-title :where [?b :block/string ?s] [?b :block/uid ?uid] [?b :block/page ?p] [?p :node/title ?page-title]]".to_string()
}

pub fn graph_page_count_query() -> String {
    "[:find (count ?e) :where [?e :node/title]]".to_string()
}

pub fn graph_block_count_query() -> String {
    "[:find (count ?b) :where [?b :block/string]]".to_string()
}

fn page_selector() -> String {
    "[:block/uid :node/title :block/string {:block/children [:block/uid :block/string :block/order :block/open {:block/refs [:block/uid :node/title :block/string]} {:block/children ...}]}]".to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn daily_note_uid_formats_correctly() {
        assert_eq!(daily_note_uid_for_date(2, 21, 2026), "02-21-2026");
        assert_eq!(daily_note_uid_for_date(12, 1, 2025), "12-01-2025");
        assert_eq!(daily_note_uid_for_date(1, 5, 2026), "01-05-2026");
    }

    #[test]
    fn pull_daily_note_returns_correct_eid_format() {
        let (eid, _selector) = pull_daily_note("02-21-2026");
        assert_eq!(
            eid,
            serde_json::Value::String("[:block/uid \"02-21-2026\"]".into())
        );
    }

    #[test]
    fn pull_daily_note_selector_includes_children() {
        let (_eid, selector) = pull_daily_note("02-21-2026");
        assert!(selector.contains(":node/title"));
        assert!(selector.contains(":block/uid"));
        assert!(selector.contains(":block/children"));
        assert!(selector.contains(":block/string"));
        assert!(selector.contains(":block/order"));
    }

    #[test]
    fn pull_selector_includes_block_refs() {
        let (_eid, selector) = pull_daily_note("02-21-2026");
        assert!(selector.contains(":block/refs"));
    }

    #[test]
    fn pull_daily_note_works_with_generated_uid() {
        let uid = daily_note_uid_for_date(2, 21, 2026);
        let (eid, _) = pull_daily_note(&uid);
        assert_eq!(
            eid,
            serde_json::Value::String("[:block/uid \"02-21-2026\"]".into())
        );
    }

    #[test]
    fn pull_page_by_title_uses_node_title_eid() {
        let (eid, _selector) = pull_page_by_title("My Page");
        assert_eq!(
            eid,
            serde_json::Value::String("[:node/title \"My Page\"]".into())
        );
    }

    #[test]
    fn pull_page_by_title_uses_same_selector_as_daily_note() {
        let (_, daily_selector) = pull_daily_note("02-21-2026");
        let (_, page_selector) = pull_page_by_title("My Page");
        assert_eq!(daily_selector, page_selector);
    }

    #[test]
    fn pull_page_by_title_with_special_chars() {
        let (eid, _) = pull_page_by_title("C++ / Rust");
        assert_eq!(
            eid,
            serde_json::Value::String("[:node/title \"C++ / Rust\"]".into())
        );
    }

    #[test]
    fn linked_refs_query_contains_page_title() {
        let q = linked_refs_query("My Page");
        assert!(q.contains("\"My Page\""));
        assert!(q.contains("?uid"));
        assert!(q.contains("?s"));
        assert!(q.contains("?page-title"));
        assert!(q.contains(":block/refs"));
        assert!(q.contains(":node/title"));
        assert!(q.contains(":block/string"));
        assert!(q.contains(":block/uid"));
        assert!(q.contains(":block/page"));
    }

    #[test]
    fn all_page_titles_query_contains_expected_clauses() {
        let q = all_page_titles_query();
        assert!(q.contains("?title"));
        assert!(q.contains("?uid"));
        assert!(q.contains(":node/title"));
        assert!(q.contains(":block/uid"));
        assert!(q.contains(":find"));
        assert!(q.contains(":where"));
    }

    #[test]
    fn linked_refs_query_escapes_double_quotes() {
        let q = linked_refs_query(r#"Page "with" quotes"#);
        assert!(q.contains(r#"Page \"with\" quotes"#));
    }
}