feat: add graphql support

This commit is contained in:
a.tolmachev
2026-03-25 20:21:07 +03:00
parent 6296b04105
commit 1ea75eb824
16 changed files with 824 additions and 30 deletions
+141 -1
View File
@@ -67,7 +67,10 @@ mod tests {
};
use axum::{Json, Router, routing::post};
use mcpaas_core::{ExecutionConfig, HttpMethod, Protocol, RestTarget, Target, ToolDescription};
use mcpaas_core::{
ExecutionConfig, GraphqlOperationType, GraphqlTarget, HttpMethod, Protocol, RestTarget,
Target, ToolDescription,
};
use mcpaas_mapping::{MappingRule, MappingSet};
use mcpaas_registry::PostgresRegistry;
use mcpaas_schema::{Schema, SchemaKind};
@@ -143,6 +146,59 @@ mod tests {
assert_eq!(test_run["response_preview"]["id"], "lead_123");
}
#[tokio::test]
async fn creates_publishes_and_tests_graphql_operation() {
let registry = test_registry().await;
let storage_root = test_storage_root("graphql");
let upstream_base_url = spawn_graphql_server().await;
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
let client = reqwest::Client::new();
let created = client
.post(format!("{base_url}/operations"))
.json(&test_graphql_operation_payload(
&upstream_base_url,
"crm_create_lead_graphql",
))
.send()
.await
.unwrap()
.json::<Value>()
.await
.unwrap();
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
let published = client
.post(format!("{base_url}/operations/{operation_id}/publish"))
.json(&json!({ "version": 1 }))
.send()
.await
.unwrap()
.json::<Value>()
.await
.unwrap();
let test_run = client
.post(format!("{base_url}/operations/{operation_id}/test-runs"))
.json(&json!({
"version": 1,
"input": { "email": "user@example.com" }
}))
.send()
.await
.unwrap()
.json::<Value>()
.await
.unwrap();
assert_eq!(published["published_version"], 1);
assert_eq!(test_run["ok"], true);
assert_eq!(
test_run["request_preview"]["variables"]["email"],
"user@example.com"
);
assert_eq!(test_run["response_preview"]["id"], "lead_123");
}
#[tokio::test]
async fn manages_auth_profiles_and_yaml_upsert() {
let registry = test_registry().await;
@@ -288,6 +344,18 @@ mod tests {
format!("http://{}", address)
}
async fn spawn_graphql_server() -> String {
let app = Router::new().route("/", post(graphql_handler));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
format!("http://{}", address)
}
async fn spawn_admin_api(app: Router) -> String {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
@@ -307,6 +375,24 @@ mod tests {
}))
}
async fn graphql_handler(Json(payload): Json<Value>) -> Json<Value> {
let email = payload
.get("variables")
.and_then(|variables| variables.get("email"))
.and_then(Value::as_str)
.unwrap_or_default();
Json(json!({
"data": {
"createLead": {
"id": "lead_123",
"status": "created",
"email": email
}
}
}))
}
async fn test_registry() -> PostgresRegistry {
let database_url = env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgres://rmcp:rmcp@127.0.0.1:5432/rmcp".to_owned());
@@ -396,6 +482,60 @@ mod tests {
}
}
fn test_graphql_operation_payload(endpoint: &str, name: &str) -> OperationPayload {
OperationPayload {
name: name.to_owned(),
display_name: "Create Lead GraphQL".to_owned(),
protocol: Protocol::Graphql,
target: Target::Graphql(GraphqlTarget {
endpoint: endpoint.to_owned(),
operation_type: GraphqlOperationType::Mutation,
operation_name: "CreateLead".to_owned(),
query_template:
"mutation CreateLead($email: String!) { createLead(email: $email) { id status email } }"
.to_owned(),
response_path: "$.response.body.data.createLead".to_owned(),
}),
input_schema: object_schema("email"),
output_schema: object_schema("id"),
input_mapping: MappingSet {
rules: vec![MappingRule {
source: "$.mcp.email".to_owned(),
target: "$.request.variables.email".to_owned(),
required: true,
default_value: None,
transform: None,
condition: None,
notes: None,
}],
},
output_mapping: MappingSet {
rules: vec![MappingRule {
source: "$.response.data.id".to_owned(),
target: "$.output.id".to_owned(),
required: true,
default_value: None,
transform: None,
condition: None,
notes: None,
}],
},
execution_config: ExecutionConfig {
timeout_ms: 1_000,
retry_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
protocol_options: None,
},
tool_description: ToolDescription {
title: "Create Lead GraphQL".to_owned(),
description: "Creates a CRM lead through GraphQL".to_owned(),
tags: vec!["crm".to_owned(), "graphql".to_owned()],
examples: Vec::new(),
},
}
}
fn object_schema(field_name: &str) -> Schema {
Schema {
kind: SchemaKind::Object,
+1
View File
@@ -612,6 +612,7 @@ fn build_request_preview(
"path": prepared.path_params,
"query": prepared.query_params,
"headers": prepared.headers,
"variables": prepared.variables.unwrap_or(Value::Null),
"body": prepared.body.unwrap_or(Value::Null)
}))
}