use async_graphql::{Context, EmptySubscription, Object, Schema, SimpleObject, ID};
use fcdb_graph::{GraphDB, Rid, LabelId, Timestamp};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(SimpleObject, Serialize, Deserialize)]
pub struct Node {
pub id: ID,
pub data: String,
pub created_at: String,
}
#[derive(SimpleObject, Serialize, Deserialize)]
pub struct GraphEdge {
pub from: ID,
pub to: ID,
pub label: String,
pub properties: String,
}
#[derive(SimpleObject, Serialize, Deserialize)]
pub struct TraversalResult {
pub node: Node,
pub depth: i32,
}
#[derive(SimpleObject, Serialize, Deserialize)]
pub struct SearchResult {
pub node: Node,
pub score: f32,
}
#[derive(async_graphql::InputObject)]
pub struct CreateNodeInput {
pub data: String,
}
#[derive(async_graphql::InputObject)]
pub struct UpdateNodeInput {
pub id: ID,
pub data: String,
}
#[derive(async_graphql::InputObject)]
pub struct CreateEdgeInput {
pub from: ID,
pub to: ID,
pub label: String,
pub properties: String,
}
#[derive(async_graphql::InputObject)]
pub struct TraverseInput {
pub from: ID,
pub labels: Option<Vec<String>>,
pub max_depth: i32,
pub as_of: Option<String>,
}
pub struct Query;
#[Object]
impl Query {
async fn node(&self, ctx: &Context<'_>, id: ID) -> async_graphql::Result<Option<Node>> {
let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
let graph = graph.read().await;
let rid = Rid(id.parse().map_err(|_| "Invalid node ID")?);
match graph.get_node(rid).await {
Ok(Some(data)) => {
let data_str = String::from_utf8(data)
.map_err(|_| "Invalid UTF-8 data")?;
Ok(Some(Node {
id,
data: data_str,
created_at: "2024-01-01T00:00:00Z".to_string(), }))
}
Ok(None) => Ok(None),
Err(e) => Err(async_graphql::Error::new(format!("Database error: {}", e))),
}
}
async fn node_at(&self, ctx: &Context<'_>, id: ID, as_of: String) -> async_graphql::Result<Option<Node>> {
let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
let graph = graph.read().await;
let rid = Rid(id.parse().map_err(|_| "Invalid node ID")?);
let timestamp = Timestamp(as_of.parse().map_err(|_| "Invalid timestamp")?);
match graph.get_node_at(rid, timestamp).await {
Ok(Some(data)) => {
let data_str = String::from_utf8(data)
.map_err(|_| "Invalid UTF-8 data")?;
Ok(Some(Node {
id,
data: data_str,
created_at: as_of,
}))
}
Ok(None) => Ok(None),
Err(e) => Err(async_graphql::Error::new(format!("Database error: {}", e))),
}
}
async fn traverse(&self, ctx: &Context<'_>, input: TraverseInput) -> async_graphql::Result<Vec<TraversalResult>> {
let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
let graph = graph.read().await;
let from_rid = Rid(input.from.parse().map_err(|_| "Invalid node ID")?);
let labels: Option<Vec<LabelId>> = input.labels.map(|ls|
ls.into_iter().map(|l| LabelId(l.parse().unwrap_or(0))).collect()
);
let max_depth = input.max_depth as usize;
let as_of = input.as_of.map(|ts| Timestamp(ts.parse().unwrap_or(0)));
let traversal = graph.traverse(from_rid, labels.as_deref(), max_depth, as_of).await
.map_err(|e| async_graphql::Error::new(format!("Traversal error: {}", e)))?;
let mut results = Vec::new();
for (rid, depth) in traversal {
if let Ok(Some(data)) = graph.get_node(rid).await {
if let Ok(data_str) = String::from_utf8(data) {
results.push(TraversalResult {
node: Node {
id: ID::from(rid.0.to_string()),
data: data_str,
created_at: "2024-01-01T00:00:00Z".to_string(),
},
depth: depth as i32,
});
}
}
}
Ok(results)
}
async fn search(&self, ctx: &Context<'_>, query: String) -> async_graphql::Result<Vec<SearchResult>> {
let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
let graph = graph.read().await;
let search_results = graph.search(&query).await
.map_err(|e| async_graphql::Error::new(format!("Search error: {}", e)))?;
let mut results = Vec::new();
for (rid, score) in search_results {
if let Ok(Some(data)) = graph.get_node(rid).await {
if let Ok(data_str) = String::from_utf8(data) {
results.push(SearchResult {
node: Node {
id: ID::from(rid.0.to_string()),
data: data_str,
created_at: "2024-01-01T00:00:00Z".to_string(),
},
score,
});
}
}
}
Ok(results)
}
}
pub struct Mutation;
#[Object]
impl Mutation {
async fn create_node(&self, ctx: &Context<'_>, input: CreateNodeInput) -> async_graphql::Result<Node> {
let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
let mut graph = graph.write().await;
let data_bytes = input.data.as_bytes();
let rid = graph.create_node(data_bytes).await
.map_err(|e| async_graphql::Error::new(format!("Create node error: {}", e)))?;
Ok(Node {
id: ID::from(rid.0.to_string()),
data: input.data,
created_at: "2024-01-01T00:00:00Z".to_string(),
})
}
async fn update_node(&self, ctx: &Context<'_>, input: UpdateNodeInput) -> async_graphql::Result<Node> {
let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
let mut graph = graph.write().await;
let rid = Rid(input.id.parse().map_err(|_| "Invalid node ID")?);
let data_bytes = input.data.as_bytes();
graph.update_node(rid, data_bytes).await
.map_err(|e| async_graphql::Error::new(format!("Update node error: {}", e)))?;
Ok(Node {
id: input.id,
data: input.data,
created_at: "2024-01-01T00:00:00Z".to_string(),
})
}
async fn create_edge(&self, ctx: &Context<'_>, input: CreateEdgeInput) -> async_graphql::Result<GraphEdge> {
let graph = ctx.data::<Arc<RwLock<GraphDB>>>()?;
let mut graph = graph.write().await;
let from_rid = Rid(input.from.parse().map_err(|_| "Invalid from ID")?);
let to_rid = Rid(input.to.parse().map_err(|_| "Invalid to ID")?);
let label_id = LabelId(input.label.parse().map_err(|_| "Invalid label")?);
let prop_bytes = input.properties.as_bytes();
graph.create_edge(from_rid, to_rid, label_id, prop_bytes).await
.map_err(|e| async_graphql::Error::new(format!("Create edge error: {}", e)))?;
Ok(GraphEdge {
from: input.from,
to: input.to,
label: input.label,
properties: input.properties,
})
}
}
pub type EnishiSchema = Schema<Query, Mutation, EmptySubscription>;
pub fn create_schema(graph: Arc<RwLock<GraphDB>>) -> EnishiSchema {
Schema::build(Query, Mutation, EmptySubscription)
.data(graph)
.finish()
}
pub const GRAPHQL_SCHEMA: &str = r#"
type Node {
id: ID!
data: String!
createdAt: String!
}
type GraphEdge {
from: ID!
to: ID!
label: String!
properties: String!
}
type TraversalResult {
node: Node!
depth: Int!
}
type SearchResult {
node: Node!
score: Float!
}
input CreateNodeInput {
data: String!
}
input UpdateNodeInput {
id: ID!
data: String!
}
input CreateEdgeInput {
from: ID!
to: ID!
label: String!
properties: String!
}
input TraverseInput {
from: ID!
labels: [String!]
maxDepth: Int!
asOf: String
}
type Query {
node(id: ID!): Node
nodeAt(id: ID!, asOf: String!): Node
traverse(input: TraverseInput!): [TraversalResult!]!
search(query: String!): [SearchResult!]!
}
type Mutation {
createNode(input: CreateNodeInput!): Node!
updateNode(input: UpdateNodeInput!): Node!
createEdge(input: CreateEdgeInput!): GraphEdge!
}
"#;
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_graphql_schema_creation() {
let temp_dir = tempdir().unwrap();
let cas = enishi_cas::PackCAS::open(temp_dir.path()).await.unwrap();
let graph = GraphDB::new(cas).await;
let graph = Arc::new(RwLock::new(graph));
let schema = create_schema(graph);
assert!(schema.query(&"query { __typename }").await.is_ok());
}
}