use anyhow::{Result, bail};
use graphql_parser::query::Field;
use serde_json::{Value, json};
use std::sync::Arc;
use uuid::Uuid;
use super::field_resolver;
use super::utils;
use crate::core::events::{FrameworkEvent, LinkEvent};
use crate::core::link::LinkEntity;
use crate::server::host::ServerHost;
pub async fn create_link_mutation(
host: &Arc<ServerHost>,
field: &Field<'_, String>,
) -> Result<Value> {
let source_id = utils::get_string_arg(field, "sourceId")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'sourceId'"))?;
let target_id = utils::get_string_arg(field, "targetId")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'targetId'"))?;
let link_type = utils::get_string_arg(field, "linkType")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'linkType'"))?;
let source_uuid = Uuid::parse_str(&source_id)?;
let target_uuid = Uuid::parse_str(&target_id)?;
let metadata = utils::get_json_arg(field, "metadata");
let link_entity = LinkEntity::new(link_type, source_uuid, target_uuid, metadata);
let created_link = host.link_service.create(link_entity).await?;
if let Some(event_bus) = host.event_bus() {
event_bus.publish(FrameworkEvent::Link(LinkEvent::Created {
link_type: created_link.link_type.clone(),
link_id: created_link.id,
source_id: created_link.source_id,
target_id: created_link.target_id,
metadata: created_link.metadata.clone(),
}));
}
Ok(json!({
"id": created_link.id.to_string(),
"sourceId": created_link.source_id.to_string(),
"targetId": created_link.target_id.to_string(),
"linkType": created_link.link_type,
"metadata": created_link.metadata,
"createdAt": created_link.created_at.to_rfc3339(),
}))
}
pub async fn delete_link_mutation(
host: &Arc<ServerHost>,
field: &Field<'_, String>,
) -> Result<Value> {
let link_id = utils::get_string_arg(field, "id")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'id'"))?;
let uuid = Uuid::parse_str(&link_id)?;
let link = host.link_service.get(&uuid).await?;
host.link_service.delete(&uuid).await?;
if let (Some(event_bus), Some(link)) = (host.event_bus(), link) {
event_bus.publish(FrameworkEvent::Link(LinkEvent::Deleted {
link_type: link.link_type,
link_id: link.id,
source_id: link.source_id,
target_id: link.target_id,
}));
}
Ok(Value::Bool(true))
}
pub async fn create_and_link_mutation(
host: &Arc<ServerHost>,
field: &Field<'_, String>,
) -> Result<Value> {
let field_name = field.name.as_str();
let parts: Vec<&str> = field_name
.strip_prefix("create")
.unwrap_or("")
.split("For")
.collect();
if parts.len() != 2 {
bail!("Invalid createAndLink mutation format: {}", field_name);
}
let entity_type = utils::pascal_to_snake(parts[0]);
let parent_type = utils::pascal_to_snake(parts[1]);
let parent_id = utils::get_string_arg(field, "parentId")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'parentId'"))?;
let data = utils::get_json_arg(field, "data")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'data'"))?;
let link_type = utils::get_string_arg(field, "linkType");
let parent_uuid = Uuid::parse_str(&parent_id)?;
if let Some(creator) = host.entity_creators.get(&entity_type) {
let created = creator.create_from_json(data).await?;
let entity_id = created
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Created entity missing id field"))?;
let entity_uuid = Uuid::parse_str(entity_id)?;
if let Some(event_bus) = host.event_bus() {
event_bus.publish(FrameworkEvent::Entity(
crate::core::events::EntityEvent::Created {
entity_type: entity_type.clone(),
entity_id: entity_uuid,
data: created.clone(),
},
));
}
let actual_link_type = if let Some(lt) = link_type {
lt
} else {
utils::find_link_type(&host.config.links, &parent_type, &entity_type)?
};
let link_entity = LinkEntity::new(actual_link_type, parent_uuid, entity_uuid, None);
let created_link = host.link_service.create(link_entity).await?;
if let Some(event_bus) = host.event_bus() {
event_bus.publish(FrameworkEvent::Link(LinkEvent::Created {
link_type: created_link.link_type,
link_id: created_link.id,
source_id: created_link.source_id,
target_id: created_link.target_id,
metadata: created_link.metadata,
}));
}
let resolved = field_resolver::resolve_entity_fields(
host,
created,
&field.selection_set.items,
&entity_type,
)
.await?;
Ok(resolved)
} else {
bail!("Unknown entity type: {}", entity_type);
}
}
pub async fn link_entities_mutation(
host: &Arc<ServerHost>,
field: &Field<'_, String>,
) -> Result<Value> {
let field_name = field.name.as_str();
let parts: Vec<&str> = field_name
.strip_prefix("link")
.unwrap_or("")
.split("To")
.collect();
if parts.len() != 2 {
bail!("Invalid link mutation format: {}", field_name);
}
let source_type = utils::pascal_to_snake(parts[0]);
let target_type = utils::pascal_to_snake(parts[1]);
let source_id = utils::get_string_arg(field, "sourceId")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'sourceId'"))?;
let target_id = utils::get_string_arg(field, "targetId")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'targetId'"))?;
let link_type = utils::get_string_arg(field, "linkType");
let source_uuid = Uuid::parse_str(&source_id)?;
let target_uuid = Uuid::parse_str(&target_id)?;
let actual_link_type = if let Some(lt) = link_type {
lt
} else {
utils::find_link_type(&host.config.links, &source_type, &target_type)?
};
let metadata = utils::get_json_arg(field, "metadata");
let link_entity = LinkEntity::new(actual_link_type, source_uuid, target_uuid, metadata);
let created_link = host.link_service.create(link_entity).await?;
if let Some(event_bus) = host.event_bus() {
event_bus.publish(FrameworkEvent::Link(LinkEvent::Created {
link_type: created_link.link_type.clone(),
link_id: created_link.id,
source_id: created_link.source_id,
target_id: created_link.target_id,
metadata: created_link.metadata.clone(),
}));
}
Ok(json!({
"id": created_link.id.to_string(),
"sourceId": created_link.source_id.to_string(),
"targetId": created_link.target_id.to_string(),
"linkType": created_link.link_type,
"metadata": created_link.metadata,
"createdAt": created_link.created_at.to_rfc3339(),
}))
}
pub async fn unlink_entities_mutation(
host: &Arc<ServerHost>,
field: &Field<'_, String>,
) -> Result<Value> {
let field_name = field.name.as_str();
let parts: Vec<&str> = field_name
.strip_prefix("unlink")
.unwrap_or("")
.split("From")
.collect();
if parts.len() != 2 {
bail!("Invalid unlink mutation format: {}", field_name);
}
let source_type = utils::pascal_to_snake(parts[0]);
let target_type = utils::pascal_to_snake(parts[1]);
let source_id = utils::get_string_arg(field, "sourceId")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'sourceId'"))?;
let target_id = utils::get_string_arg(field, "targetId")
.ok_or_else(|| anyhow::anyhow!("Missing required argument 'targetId'"))?;
let link_type = utils::get_string_arg(field, "linkType");
let source_uuid = Uuid::parse_str(&source_id)?;
let target_uuid = Uuid::parse_str(&target_id)?;
let actual_link_type = if let Some(lt) = link_type {
Some(lt)
} else {
utils::find_link_type(&host.config.links, &source_type, &target_type).ok()
};
let links = host
.link_service
.find_by_source(
&source_uuid,
actual_link_type.as_deref(),
Some(&target_type),
)
.await?;
for link in links {
if link.target_id == target_uuid {
host.link_service.delete(&link.id).await?;
if let Some(event_bus) = host.event_bus() {
event_bus.publish(FrameworkEvent::Link(LinkEvent::Deleted {
link_type: link.link_type,
link_id: link.id,
source_id: link.source_id,
target_id: link.target_id,
}));
}
return Ok(Value::Bool(true));
}
}
Ok(Value::Bool(false))
}
#[cfg(test)]
#[cfg(feature = "graphql")]
mod tests {
use super::super::core::GraphQLExecutor;
use crate::config::{EntityAuthConfig, EntityConfig, LinksConfig};
use crate::core::link::{LinkDefinition, LinkEntity};
use crate::core::service::LinkService;
use crate::core::{EntityCreator, EntityFetcher};
use crate::server::entity_registry::{EntityDescriptor, EntityRegistry};
use crate::server::host::ServerHost;
use crate::storage::in_memory::InMemoryLinkService;
use async_trait::async_trait;
use axum::Router;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
struct MockFetcher;
#[async_trait]
impl EntityFetcher for MockFetcher {
async fn fetch_as_json(&self, _entity_id: &Uuid) -> anyhow::Result<Value> {
Ok(json!({}))
}
}
struct MockCreator;
#[async_trait]
impl EntityCreator for MockCreator {
async fn create_from_json(&self, mut data: Value) -> anyhow::Result<Value> {
let id = Uuid::new_v4();
if let Some(obj) = data.as_object_mut() {
obj.insert("id".to_string(), json!(id.to_string()));
}
Ok(data)
}
async fn update_from_json(
&self,
entity_id: &Uuid,
mut data: Value,
) -> anyhow::Result<Value> {
if let Some(obj) = data.as_object_mut() {
obj.insert("id".to_string(), json!(entity_id.to_string()));
}
Ok(data)
}
async fn delete(&self, _entity_id: &Uuid) -> anyhow::Result<()> {
Ok(())
}
}
struct StubDescriptor {
entity_type: String,
plural: String,
}
impl StubDescriptor {
fn new(singular: &str, plural: &str) -> Self {
Self {
entity_type: singular.to_string(),
plural: plural.to_string(),
}
}
}
impl EntityDescriptor for StubDescriptor {
fn entity_type(&self) -> &str {
&self.entity_type
}
fn plural(&self) -> &str {
&self.plural
}
fn build_routes(&self) -> Router {
Router::new()
}
}
fn build_test_host_with_link_service(
link_service: Arc<InMemoryLinkService>,
) -> Arc<ServerHost> {
let config = LinksConfig {
entities: vec![
EntityConfig {
singular: "order".to_string(),
plural: "orders".to_string(),
auth: EntityAuthConfig::default(),
},
EntityConfig {
singular: "invoice".to_string(),
plural: "invoices".to_string(),
auth: EntityAuthConfig::default(),
},
],
links: vec![LinkDefinition {
link_type: "has_invoice".to_string(),
source_type: "order".to_string(),
target_type: "invoice".to_string(),
forward_route_name: "invoices".to_string(),
reverse_route_name: "order".to_string(),
description: None,
required_fields: None,
auth: None,
}],
validation_rules: None,
events: None,
sinks: None,
};
let mut registry = EntityRegistry::new();
registry.register(Box::new(StubDescriptor::new("order", "orders")));
registry.register(Box::new(StubDescriptor::new("invoice", "invoices")));
let mut fetchers: HashMap<String, Arc<dyn EntityFetcher>> = HashMap::new();
fetchers.insert("order".to_string(), Arc::new(MockFetcher));
fetchers.insert("invoice".to_string(), Arc::new(MockFetcher));
let mut creators: HashMap<String, Arc<dyn EntityCreator>> = HashMap::new();
creators.insert("order".to_string(), Arc::new(MockCreator));
creators.insert("invoice".to_string(), Arc::new(MockCreator));
Arc::new(
ServerHost::from_builder_components(link_service, config, registry, fetchers, creators)
.expect("should build test host"),
)
}
fn default_host() -> (Arc<ServerHost>, Arc<InMemoryLinkService>) {
let link_service = Arc::new(InMemoryLinkService::new());
let host = build_test_host_with_link_service(link_service.clone());
(host, link_service)
}
#[tokio::test]
async fn test_create_link_mutation_success() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createLink(sourceId: "{}", targetId: "{}", linkType: "has_invoice") {{ id }} }}"#,
source_id, target_id
);
let result = executor
.execute(&query, None)
.await
.expect("should create link");
let link_result = result
.get("data")
.and_then(|d| d.get("createLink"))
.expect("should have createLink");
assert!(link_result.get("id").is_some(), "should have id");
}
#[tokio::test]
async fn test_create_link_mutation_missing_source_id() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createLink(targetId: "{}", linkType: "has_invoice") {{ id }} }}"#,
target_id
);
let result = executor.execute(&query, None).await;
assert!(result.is_err(), "missing sourceId should error");
let err_msg = result.expect_err("error").to_string();
assert!(
err_msg.contains("sourceId"),
"should mention sourceId: {}",
err_msg
);
}
#[tokio::test]
async fn test_create_link_mutation_missing_link_type() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createLink(sourceId: "{}", targetId: "{}") {{ id }} }}"#,
source_id, target_id
);
let result = executor.execute(&query, None).await;
assert!(result.is_err(), "missing linkType should error");
let err_msg = result.expect_err("error").to_string();
assert!(
err_msg.contains("linkType"),
"should mention linkType: {}",
err_msg
);
}
#[tokio::test]
async fn test_delete_link_mutation_direct_call() {
use super::delete_link_mutation;
use graphql_parser::Pos;
use graphql_parser::query::{SelectionSet, Value as GqlValue};
let (host, link_service) = default_host();
let link = LinkEntity::new("has_invoice", Uuid::new_v4(), Uuid::new_v4(), None);
let created = link_service.create(link).await.expect("should create link");
let pos = Pos { line: 1, column: 1 };
let field = graphql_parser::query::Field {
position: pos,
alias: None,
name: "deleteLink".to_string(),
arguments: vec![("id".to_string(), GqlValue::String(created.id.to_string()))],
directives: vec![],
selection_set: SelectionSet {
span: (pos, pos),
items: vec![],
},
};
let result = delete_link_mutation(&host, &field)
.await
.expect("should delete link");
assert_eq!(result, Value::Bool(true));
}
#[tokio::test]
async fn test_delete_link_mutation_missing_id_direct_call() {
use super::delete_link_mutation;
use graphql_parser::Pos;
use graphql_parser::query::SelectionSet;
let (host, _) = default_host();
let pos = Pos { line: 1, column: 1 };
let field = graphql_parser::query::Field {
position: pos,
alias: None,
name: "deleteLink".to_string(),
arguments: vec![],
directives: vec![],
selection_set: SelectionSet {
span: (pos, pos),
items: vec![],
},
};
let result = delete_link_mutation(&host, &field).await;
assert!(result.is_err(), "missing id should error");
}
#[tokio::test]
async fn test_create_and_link_mutation_success() {
let (host, link_service) = default_host();
let executor = GraphQLExecutor::new(host).await;
let parent_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createInvoiceForOrder(parentId: "{}", data: {{amount: 100}}) {{ id }} }}"#,
parent_id
);
let result = executor
.execute(&query, None)
.await
.expect("should create and link");
let created = result
.get("data")
.and_then(|d| d.get("createInvoiceForOrder"))
.expect("should have createInvoiceForOrder");
assert!(created.get("id").is_some(), "created entity should have id");
let links = link_service
.find_by_source(&parent_id, Some("has_invoice"), None)
.await
.expect("should find links");
assert_eq!(links.len(), 1, "should have one link from parent");
}
#[tokio::test]
async fn test_create_and_link_mutation_missing_parent_id() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let result = executor
.execute(
r#"mutation { createInvoiceForOrder(data: {amount: 100}) { id } }"#,
None,
)
.await;
assert!(result.is_err(), "missing parentId should error");
let err_msg = result.expect_err("error").to_string();
assert!(
err_msg.contains("parentId"),
"should mention parentId: {}",
err_msg
);
}
#[tokio::test]
async fn test_create_and_link_mutation_missing_data() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let parent_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createInvoiceForOrder(parentId: "{}") {{ id }} }}"#,
parent_id
);
let result = executor.execute(&query, None).await;
assert!(result.is_err(), "missing data should error");
}
#[tokio::test]
async fn test_create_and_link_mutation_unknown_entity_type() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let parent_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createWidgetForGadget(parentId: "{}", data: {{name: "w"}}) {{ id }} }}"#,
parent_id
);
let result = executor.execute(&query, None).await;
assert!(result.is_err(), "unknown entity type should error");
}
#[tokio::test]
async fn test_create_and_link_mutation_with_explicit_link_type() {
let (host, link_service) = default_host();
let executor = GraphQLExecutor::new(host).await;
let parent_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createInvoiceForOrder(parentId: "{}", data: {{amount: 200}}, linkType: "has_invoice") {{ id }} }}"#,
parent_id
);
let result = executor
.execute(&query, None)
.await
.expect("should succeed with explicit linkType");
let created = result
.get("data")
.and_then(|d| d.get("createInvoiceForOrder"))
.expect("should have result");
assert!(created.get("id").is_some());
let links = link_service
.find_by_source(&parent_id, Some("has_invoice"), None)
.await
.expect("should find links");
assert_eq!(links.len(), 1);
}
#[tokio::test]
async fn test_link_entities_mutation_success() {
let (host, link_service) = default_host();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ linkOrderToInvoice(sourceId: "{}", targetId: "{}") {{ id }} }}"#,
source_id, target_id
);
let result = executor
.execute(&query, None)
.await
.expect("should link entities");
let link_result = result
.get("data")
.and_then(|d| d.get("linkOrderToInvoice"))
.expect("should have result");
assert!(link_result.get("id").is_some(), "link should have id");
let links = link_service
.find_by_source(&source_id, Some("has_invoice"), None)
.await
.expect("should find links");
assert_eq!(links.len(), 1);
}
#[tokio::test]
async fn test_link_entities_mutation_missing_source_id() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ linkOrderToInvoice(targetId: "{}") {{ id }} }}"#,
target_id
);
let result = executor.execute(&query, None).await;
assert!(result.is_err(), "missing sourceId should error");
}
#[tokio::test]
async fn test_link_entities_mutation_with_explicit_link_type() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ linkOrderToInvoice(sourceId: "{}", targetId: "{}", linkType: "has_invoice") {{ id }} }}"#,
source_id, target_id
);
let result = executor
.execute(&query, None)
.await
.expect("should succeed with explicit linkType");
let link_result = result
.get("data")
.and_then(|d| d.get("linkOrderToInvoice"))
.expect("should have result");
assert!(link_result.get("id").is_some());
}
#[tokio::test]
async fn test_unlink_entities_mutation_found_and_deleted() {
let (host, link_service) = default_host();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let link = LinkEntity::new("has_invoice", source_id, target_id, None);
link_service.create(link).await.expect("should create link");
let query = format!(
r#"mutation {{ unlinkOrderFromInvoice(sourceId: "{}", targetId: "{}") }}"#,
source_id, target_id
);
let result = executor
.execute(&query, None)
.await
.expect("should succeed");
let unlink_result = result
.get("data")
.and_then(|d| d.get("unlinkOrderFromInvoice"))
.expect("should have result");
assert_eq!(
*unlink_result,
Value::Bool(true),
"should return true when link found and deleted"
);
}
#[tokio::test]
async fn test_unlink_entities_mutation_no_link_found() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ unlinkOrderFromInvoice(sourceId: "{}", targetId: "{}") }}"#,
source_id, target_id
);
let result = executor
.execute(&query, None)
.await
.expect("should succeed even without link");
let unlink_result = result
.get("data")
.and_then(|d| d.get("unlinkOrderFromInvoice"))
.expect("should have result");
assert_eq!(
*unlink_result,
Value::Bool(false),
"should return false when no link found"
);
}
#[tokio::test]
async fn test_unlink_entities_mutation_missing_source_id() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ unlinkOrderFromInvoice(targetId: "{}") }}"#,
target_id
);
let result = executor.execute(&query, None).await;
assert!(result.is_err(), "missing sourceId should error");
}
#[tokio::test]
async fn test_unlink_entities_mutation_missing_target_id() {
let (host, _) = default_host();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ unlinkOrderFromInvoice(sourceId: "{}") }}"#,
source_id
);
let result = executor.execute(&query, None).await;
assert!(result.is_err(), "missing targetId should error");
}
fn build_host_with_event_bus(link_service: Arc<InMemoryLinkService>) -> Arc<ServerHost> {
use crate::core::events::EventBus;
let config = LinksConfig {
entities: vec![
EntityConfig {
singular: "order".to_string(),
plural: "orders".to_string(),
auth: EntityAuthConfig::default(),
},
EntityConfig {
singular: "invoice".to_string(),
plural: "invoices".to_string(),
auth: EntityAuthConfig::default(),
},
],
links: vec![LinkDefinition {
link_type: "has_invoice".to_string(),
source_type: "order".to_string(),
target_type: "invoice".to_string(),
forward_route_name: "invoices".to_string(),
reverse_route_name: "order".to_string(),
description: None,
required_fields: None,
auth: None,
}],
validation_rules: None,
events: None,
sinks: None,
};
let mut registry = EntityRegistry::new();
registry.register(Box::new(StubDescriptor::new("order", "orders")));
registry.register(Box::new(StubDescriptor::new("invoice", "invoices")));
let mut fetchers: HashMap<String, Arc<dyn EntityFetcher>> = HashMap::new();
fetchers.insert("order".to_string(), Arc::new(MockFetcher));
fetchers.insert("invoice".to_string(), Arc::new(MockFetcher));
let mut creators: HashMap<String, Arc<dyn EntityCreator>> = HashMap::new();
creators.insert("order".to_string(), Arc::new(MockCreator));
creators.insert("invoice".to_string(), Arc::new(MockCreator));
Arc::new(
ServerHost::from_builder_components(link_service, config, registry, fetchers, creators)
.expect("should build test host")
.with_event_bus(EventBus::new(256)),
)
}
fn host_with_event_bus() -> (Arc<ServerHost>, Arc<InMemoryLinkService>) {
let link_service = Arc::new(InMemoryLinkService::new());
let host = build_host_with_event_bus(link_service.clone());
(host, link_service)
}
#[tokio::test]
async fn test_create_link_with_event_bus() {
let (host, _) = host_with_event_bus();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createLink(sourceId: "{}", targetId: "{}", linkType: "has_invoice") {{ id }} }}"#,
source_id, target_id
);
let result = executor
.execute(&query, None)
.await
.expect("should create link with EventBus");
let link_result = result
.get("data")
.and_then(|d| d.get("createLink"))
.expect("should have createLink");
assert!(link_result.get("id").is_some());
}
#[tokio::test]
async fn test_delete_link_with_event_bus() {
use super::delete_link_mutation;
use graphql_parser::Pos;
use graphql_parser::query::{SelectionSet, Value as GqlValue};
let (host, link_service) = host_with_event_bus();
let link = LinkEntity::new("has_invoice", Uuid::new_v4(), Uuid::new_v4(), None);
let created = link_service.create(link).await.expect("should create link");
let pos = Pos { line: 1, column: 1 };
let field = graphql_parser::query::Field {
position: pos,
alias: None,
name: "deleteLink".to_string(),
arguments: vec![("id".to_string(), GqlValue::String(created.id.to_string()))],
directives: vec![],
selection_set: SelectionSet {
span: (pos, pos),
items: vec![],
},
};
let result = delete_link_mutation(&host, &field)
.await
.expect("should delete link with EventBus");
assert_eq!(result, Value::Bool(true));
}
#[tokio::test]
async fn test_create_and_link_with_event_bus() {
let (host, link_service) = host_with_event_bus();
let executor = GraphQLExecutor::new(host).await;
let parent_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ createInvoiceForOrder(parentId: "{}", data: {{amount: 100}}) {{ id }} }}"#,
parent_id
);
let result = executor
.execute(&query, None)
.await
.expect("should create and link with EventBus");
let created = result
.get("data")
.and_then(|d| d.get("createInvoiceForOrder"))
.expect("should have result");
assert!(created.get("id").is_some());
let links = link_service
.find_by_source(&parent_id, Some("has_invoice"), None)
.await
.expect("should find links");
assert_eq!(links.len(), 1);
}
#[tokio::test]
async fn test_link_entities_with_event_bus() {
let (host, _) = host_with_event_bus();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let query = format!(
r#"mutation {{ linkOrderToInvoice(sourceId: "{}", targetId: "{}") {{ id }} }}"#,
source_id, target_id
);
let result = executor
.execute(&query, None)
.await
.expect("should link entities with EventBus");
let link_result = result
.get("data")
.and_then(|d| d.get("linkOrderToInvoice"))
.expect("should have result");
assert!(link_result.get("id").is_some());
}
#[tokio::test]
async fn test_unlink_entities_with_event_bus() {
let (host, link_service) = host_with_event_bus();
let executor = GraphQLExecutor::new(host).await;
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let link = LinkEntity::new("has_invoice", source_id, target_id, None);
link_service.create(link).await.expect("should create link");
let query = format!(
r#"mutation {{ unlinkOrderFromInvoice(sourceId: "{}", targetId: "{}") }}"#,
source_id, target_id
);
let result = executor
.execute(&query, None)
.await
.expect("should unlink with EventBus");
let unlink_result = result
.get("data")
.and_then(|d| d.get("unlinkOrderFromInvoice"))
.expect("should have result");
assert_eq!(*unlink_result, Value::Bool(true));
}
}