use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::mcp::clients::services::NotionApiService;
use crate::mcp::core::{error::WorkflowError, nodes::Node, task::TaskContext};
#[derive(Debug)]
pub struct NotionServiceNode {
api_service: Arc<RwLock<NotionApiService>>,
}
impl NotionServiceNode {
pub fn new(api_key: String) -> Self {
Self {
api_service: Arc::new(RwLock::new(NotionApiService::new(api_key))),
}
}
pub async fn search_pages(&self, query: &str) -> Result<Value, WorkflowError> {
let api = self.api_service.read().await;
let search_result = api
.search(query, Some("page"), None, None, Some(20))
.await?;
let pages = search_result
.results
.into_iter()
.filter_map(|result| {
match result {
crate::mcp::clients::services::notion_api::SearchResult::Page(page) => {
let title = Self::extract_page_title(&page.properties);
Some(serde_json::json!({
"id": page.id,
"title": title,
"url": page.url,
"created_time": page.created_time,
"last_edited_time": page.last_edited_time
}))
}
_ => None,
}
})
.collect::<Vec<_>>();
Ok(serde_json::json!({
"source": "notion",
"query": query,
"results_found": pages.len(),
"pages": pages,
"real_api": true
}))
}
pub async fn search_databases(&self, query: &str) -> Result<Value, WorkflowError> {
let api = self.api_service.read().await;
let search_result = api
.search(query, Some("database"), None, None, Some(20))
.await?;
let databases = search_result
.results
.into_iter()
.filter_map(|result| match result {
crate::mcp::clients::services::notion_api::SearchResult::Database(db) => {
let title = db
.title
.first()
.and_then(|rt| rt.plain_text.clone())
.unwrap_or_else(|| "Untitled".to_string());
Some(serde_json::json!({
"id": db.id,
"title": title,
"url": db.url,
"created_time": db.created_time,
"last_edited_time": db.last_edited_time
}))
}
_ => None,
})
.collect::<Vec<_>>();
Ok(serde_json::json!({
"source": "notion",
"query": query,
"results_found": databases.len(),
"databases": databases,
"real_api": true
}))
}
fn extract_page_title(properties: &Value) -> String {
let title_keys = ["Name", "Title", "title", "name"];
for key in &title_keys {
if let Some(prop) = properties.get(key) {
if let Some(title_array) = prop.get("title").and_then(|t| t.as_array()) {
if let Some(first) = title_array.first() {
if let Some(text) = first.get("plain_text").and_then(|t| t.as_str()) {
return text.to_string();
}
}
}
}
}
"Untitled".to_string()
}
}
#[async_trait]
impl Node for NotionServiceNode {
async fn execute(&self, input: Value, _context: &TaskContext) -> Result<Value, WorkflowError> {
let operation = input
.get("operation")
.and_then(|v| v.as_str())
.unwrap_or("search");
match operation {
"search" | "search_pages" => {
let query = input
.get("query")
.or_else(|| input.get("user_query"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
WorkflowError::InvalidInput("Missing query parameter".to_string())
})?;
let results = self.search_pages(query).await?;
Ok(serde_json::json!({
"notion_search_results": results,
"notion_search_completed": true
}))
}
"search_databases" => {
let query = input.get("query").and_then(|v| v.as_str()).ok_or_else(|| {
WorkflowError::InvalidInput("Missing query parameter".to_string())
})?;
let results = self.search_databases(query).await?;
Ok(serde_json::json!({
"notion_search_results": results,
"notion_search_completed": true
}))
}
"get_page" => {
let page_id = input
.get("page_id")
.and_then(|v| v.as_str())
.ok_or_else(|| {
WorkflowError::InvalidInput("Missing page_id parameter".to_string())
})?;
let api = self.api_service.read().await;
let page = api.get_page(page_id).await?;
Ok(serde_json::json!({
"page": page,
"operation_completed": true
}))
}
"create_page" => {
let title = input.get("title").and_then(|v| v.as_str()).ok_or_else(|| {
WorkflowError::InvalidInput("Missing title parameter".to_string())
})?;
let content = input.get("content").and_then(|v| v.as_str()).unwrap_or("");
let parent_id = input
.get("parent_id")
.or_else(|| input.get("database_id"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
WorkflowError::InvalidInput("Missing parent_id parameter".to_string())
})?;
let api = self.api_service.read().await;
let parent = crate::mcp::clients::services::notion_api::Parent::Database {
database_id: parent_id.to_string(),
};
let properties = serde_json::json!({
"Name": NotionApiService::create_title_property(title)
});
let children = if !content.is_empty() {
Some(vec![
crate::mcp::clients::services::notion_api::Block::Paragraph {
paragraph: crate::mcp::clients::services::notion_api::ParagraphBlock {
rich_text: NotionApiService::create_rich_text(content),
},
},
])
} else {
None
};
let page = api.create_page(parent, properties, children).await?;
Ok(serde_json::json!({
"page": page,
"operation_completed": true
}))
}
_ => Err(WorkflowError::InvalidInput(format!(
"Unknown operation: {}",
operation
))),
}
}
fn name(&self) -> &str {
"NotionServiceNode"
}
}
#[cfg(test)]
mod tests {
use super::*;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn test_search_pages() {
let mock_server = MockServer::start().await;
let api_service =
NotionApiService::with_base_url("test-api-key".to_string(), mock_server.uri());
let node = NotionServiceNode {
api_service: Arc::new(RwLock::new(api_service)),
};
Mock::given(method("POST"))
.and(path("/v1/search"))
.and(header("Authorization", "Bearer test-api-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"results": [{
"object": "page",
"id": "123",
"created_time": "2024-01-01T00:00:00Z",
"last_edited_time": "2024-01-01T00:00:00Z",
"archived": false,
"properties": {
"Name": {
"title": [{
"plain_text": "Test Page"
}]
}
},
"parent": {
"type": "workspace",
"workspace": true
},
"url": "https://notion.so/123"
}],
"has_more": false,
"next_cursor": null
})))
.mount(&mock_server)
.await;
let input = serde_json::json!({
"operation": "search_pages",
"query": "test query"
});
let result = node.execute(input, &TaskContext::default()).await.unwrap();
let search_results = result.get("notion_search_results").unwrap();
assert_eq!(search_results.get("query").unwrap(), "test query");
assert_eq!(search_results.get("results_found").unwrap(), 1);
assert!(search_results.get("real_api").unwrap().as_bool().unwrap());
let pages = search_results.get("pages").unwrap().as_array().unwrap();
assert_eq!(pages[0].get("title").unwrap(), "Test Page");
}
}