Skip to main content

fcdb_api/
lib.rs

1//! # Enishi API
2//!
3//! GraphQL and gRPC API interfaces for the Enishi database.
4//!
5//! Merkle DAG: enishi_api -> graphql_schema, grpc_services, http_handlers
6
7use async_graphql::{Context, EmptySubscription, Object, Schema, SimpleObject, ID};
8use fcdb_graph::{GraphDB, Rid, LabelId, Timestamp};
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// GraphQL node representation
14#[derive(SimpleObject, Serialize, Deserialize)]
15pub struct Node {
16    /// Unique identifier
17    pub id: ID,
18    /// Node data as JSON string
19    pub data: String,
20    /// Creation timestamp
21    pub created_at: String,
22}
23
24/// GraphQL edge representation
25#[derive(SimpleObject, Serialize, Deserialize)]
26pub struct GraphEdge {
27    /// Source node ID
28    pub from: ID,
29    /// Target node ID
30    pub to: ID,
31    /// Edge label
32    pub label: String,
33    /// Edge properties as JSON string
34    pub properties: String,
35}
36
37/// GraphQL traversal result
38#[derive(SimpleObject, Serialize, Deserialize)]
39pub struct TraversalResult {
40    /// Node that was visited
41    pub node: Node,
42    /// Depth at which this node was found
43    pub depth: i32,
44}
45
46/// GraphQL search result
47#[derive(SimpleObject, Serialize, Deserialize)]
48pub struct SearchResult {
49    /// Matching node
50    pub node: Node,
51    /// Search score
52    pub score: f32,
53}
54
55/// Input for creating nodes
56#[derive(async_graphql::InputObject)]
57pub struct CreateNodeInput {
58    /// Node data as JSON string
59    pub data: String,
60}
61
62/// Input for updating nodes
63#[derive(async_graphql::InputObject)]
64pub struct UpdateNodeInput {
65    /// Node ID
66    pub id: ID,
67    /// New node data as JSON string
68    pub data: String,
69}
70
71/// Input for creating edges
72#[derive(async_graphql::InputObject)]
73pub struct CreateEdgeInput {
74    /// Source node ID
75    pub from: ID,
76    /// Target node ID
77    pub to: ID,
78    /// Edge label
79    pub label: String,
80    /// Edge properties as JSON string
81    pub properties: String,
82}
83
84/// Input for traversal queries
85#[derive(async_graphql::InputObject)]
86pub struct TraverseInput {
87    /// Starting node ID
88    pub from: ID,
89    /// Edge labels to follow (empty means all)
90    pub labels: Option<Vec<String>>,
91    /// Maximum traversal depth
92    pub max_depth: i32,
93    /// Historical timestamp (optional)
94    pub as_of: Option<String>,
95}
96
97/// GraphQL query root
98pub struct Query;
99
100#[Object]
101impl Query {
102    /// Get a node by ID
103    async fn node(&self, ctx: &Context<'_>, id: ID) -> async_graphql::Result<Option<Node>> {
104        let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
105        let graph = graph.read().await;
106
107        let rid = Rid(id.parse().map_err(|_| "Invalid node ID")?);
108
109        match graph.get_node(rid).await {
110            Ok(Some(data)) => {
111                let data_str = String::from_utf8(data)
112                    .map_err(|_| "Invalid UTF-8 data")?;
113                Ok(Some(Node {
114                    id,
115                    data: data_str,
116                    created_at: "2024-01-01T00:00:00Z".to_string(), // Simplified
117                }))
118            }
119            Ok(None) => Ok(None),
120            Err(e) => Err(async_graphql::Error::new(format!("Database error: {}", e))),
121        }
122    }
123
124    /// Get a node at a specific historical timestamp
125    async fn node_at(&self, ctx: &Context<'_>, id: ID, as_of: String) -> async_graphql::Result<Option<Node>> {
126        let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
127        let graph = graph.read().await;
128
129        let rid = Rid(id.parse().map_err(|_| "Invalid node ID")?);
130        let timestamp = Timestamp(as_of.parse().map_err(|_| "Invalid timestamp")?);
131
132        match graph.get_node_at(rid, timestamp).await {
133            Ok(Some(data)) => {
134                let data_str = String::from_utf8(data)
135                    .map_err(|_| "Invalid UTF-8 data")?;
136                Ok(Some(Node {
137                    id,
138                    data: data_str,
139                    created_at: as_of,
140                }))
141            }
142            Ok(None) => Ok(None),
143            Err(e) => Err(async_graphql::Error::new(format!("Database error: {}", e))),
144        }
145    }
146
147    /// Traverse the graph from a starting node
148    async fn traverse(&self, ctx: &Context<'_>, input: TraverseInput) -> async_graphql::Result<Vec<TraversalResult>> {
149        let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
150        let graph = graph.read().await;
151
152        let from_rid = Rid(input.from.parse().map_err(|_| "Invalid node ID")?);
153        let labels: Option<Vec<LabelId>> = input.labels.map(|ls|
154            ls.into_iter().map(|l| LabelId(l.parse().unwrap_or(0))).collect()
155        );
156        let max_depth = input.max_depth as usize;
157        let as_of = input.as_of.map(|ts| Timestamp(ts.parse().unwrap_or(0)));
158
159        let traversal = graph.traverse(from_rid, labels.as_deref(), max_depth, as_of).await
160            .map_err(|e| async_graphql::Error::new(format!("Traversal error: {}", e)))?;
161
162        let mut results = Vec::new();
163        for (rid, depth) in traversal {
164            // Get node data for each result
165            if let Ok(Some(data)) = graph.get_node(rid).await {
166                if let Ok(data_str) = String::from_utf8(data) {
167                    results.push(TraversalResult {
168                        node: Node {
169                            id: ID::from(rid.0.to_string()),
170                            data: data_str,
171                            created_at: "2024-01-01T00:00:00Z".to_string(),
172                        },
173                        depth: depth as i32,
174                    });
175                }
176            }
177        }
178
179        Ok(results)
180    }
181
182    /// Search nodes by text content
183    async fn search(&self, ctx: &Context<'_>, query: String) -> async_graphql::Result<Vec<SearchResult>> {
184        let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
185        let graph = graph.read().await;
186
187        let search_results = graph.search(&query).await
188            .map_err(|e| async_graphql::Error::new(format!("Search error: {}", e)))?;
189
190        let mut results = Vec::new();
191        for (rid, score) in search_results {
192            if let Ok(Some(data)) = graph.get_node(rid).await {
193                if let Ok(data_str) = String::from_utf8(data) {
194                    results.push(SearchResult {
195                        node: Node {
196                            id: ID::from(rid.0.to_string()),
197                            data: data_str,
198                            created_at: "2024-01-01T00:00:00Z".to_string(),
199                        },
200                        score,
201                    });
202                }
203            }
204        }
205
206        Ok(results)
207    }
208}
209
210/// GraphQL mutation root
211pub struct Mutation;
212
213#[Object]
214impl Mutation {
215    /// Create a new node
216    async fn create_node(&self, ctx: &Context<'_>, input: CreateNodeInput) -> async_graphql::Result<Node> {
217        let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
218        let mut graph = graph.write().await;
219
220        let data_bytes = input.data.as_bytes();
221        let rid = graph.create_node(data_bytes).await
222            .map_err(|e| async_graphql::Error::new(format!("Create node error: {}", e)))?;
223
224        Ok(Node {
225            id: ID::from(rid.0.to_string()),
226            data: input.data,
227            created_at: "2024-01-01T00:00:00Z".to_string(),
228        })
229    }
230
231    /// Update an existing node
232    async fn update_node(&self, ctx: &Context<'_>, input: UpdateNodeInput) -> async_graphql::Result<Node> {
233        let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
234        let mut graph = graph.write().await;
235
236        let rid = Rid(input.id.parse().map_err(|_| "Invalid node ID")?);
237        let data_bytes = input.data.as_bytes();
238
239        graph.update_node(rid, data_bytes).await
240            .map_err(|e| async_graphql::Error::new(format!("Update node error: {}", e)))?;
241
242        Ok(Node {
243            id: input.id,
244            data: input.data,
245            created_at: "2024-01-01T00:00:00Z".to_string(),
246        })
247    }
248
249    /// Create an edge between nodes
250    async fn create_edge(&self, ctx: &Context<'_>, input: CreateEdgeInput) -> async_graphql::Result<GraphEdge> {
251        let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
252        let mut graph = graph.write().await;
253
254        let from_rid = Rid(input.from.parse().map_err(|_| "Invalid from ID")?);
255        let to_rid = Rid(input.to.parse().map_err(|_| "Invalid to ID")?);
256        let label_id = LabelId(input.label.parse().map_err(|_| "Invalid label")?);
257        let prop_bytes = input.properties.as_bytes();
258
259        graph.create_edge(from_rid, to_rid, label_id, prop_bytes).await
260            .map_err(|e| async_graphql::Error::new(format!("Create edge error: {}", e)))?;
261
262        Ok(GraphEdge {
263            from: input.from,
264            to: input.to,
265            label: input.label,
266            properties: input.properties,
267        })
268    }
269}
270
271/// GraphQL schema type
272pub type EnishiSchema = Schema<Query, Mutation, EmptySubscription>;
273
274/// Create the GraphQL schema
275pub fn create_schema(graph: Arc<RwLock<GraphDB>>) -> EnishiSchema {
276    Schema::build(Query, Mutation, EmptySubscription)
277        .data(graph)
278        .finish()
279}
280
281/// GraphQL SDL (Schema Definition Language)
282pub const GRAPHQL_SCHEMA: &str = r#"
283    type Node {
284        id: ID!
285        data: String!
286        createdAt: String!
287    }
288
289    type GraphEdge {
290        from: ID!
291        to: ID!
292        label: String!
293        properties: String!
294    }
295
296    type TraversalResult {
297        node: Node!
298        depth: Int!
299    }
300
301    type SearchResult {
302        node: Node!
303        score: Float!
304    }
305
306    input CreateNodeInput {
307        data: String!
308    }
309
310    input UpdateNodeInput {
311        id: ID!
312        data: String!
313    }
314
315    input CreateEdgeInput {
316        from: ID!
317        to: ID!
318        label: String!
319        properties: String!
320    }
321
322    input TraverseInput {
323        from: ID!
324        labels: [String!]
325        maxDepth: Int!
326        asOf: String
327    }
328
329    type Query {
330        node(id: ID!): Node
331        nodeAt(id: ID!, asOf: String!): Node
332        traverse(input: TraverseInput!): [TraversalResult!]!
333        search(query: String!): [SearchResult!]!
334    }
335
336    type Mutation {
337        createNode(input: CreateNodeInput!): Node!
338        updateNode(input: UpdateNodeInput!): Node!
339        createEdge(input: CreateEdgeInput!): GraphEdge!
340    }
341"#;
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use tempfile::tempdir;
347
348    #[tokio::test]
349    async fn test_graphql_schema_creation() {
350        let temp_dir = tempdir().unwrap();
351        let cas = enishi_cas::PackCAS::open(temp_dir.path()).await.unwrap();
352        let graph = GraphDB::new(cas).await;
353        let graph = Arc::new(RwLock::new(graph));
354
355        let schema = create_schema(graph);
356        assert!(schema.query(&"query { __typename }").await.is_ok());
357    }
358}