feat: add operations mutation api for alpine ui
This commit is contained in:
+164
-11
@@ -1,6 +1,6 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
routing::{delete, get, post},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -16,10 +16,10 @@ use crate::{
|
||||
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
|
||||
observability::{get_agent_usage, get_log, get_operation_usage, get_usage, list_logs},
|
||||
operations::{
|
||||
create_operation, create_version, export_operation, generate_draft, get_operation,
|
||||
get_operation_version, list_grpc_services, list_operations, publish_operation,
|
||||
run_test, upload_descriptor_set, upload_input_json, upload_output_json,
|
||||
upload_proto_descriptor,
|
||||
archive_operation, create_operation, create_version, delete_operation,
|
||||
export_operation, generate_draft, get_operation, get_operation_version,
|
||||
list_grpc_services, list_operations, publish_operation, run_test, update_operation,
|
||||
upload_descriptor_set, upload_input_json, upload_output_json, upload_proto_descriptor,
|
||||
},
|
||||
workspaces::{create_workspace, get_workspace, list_workspaces, update_workspace},
|
||||
},
|
||||
@@ -30,7 +30,12 @@ pub fn build_app(state: AppState) -> Router {
|
||||
let workspace_router = Router::new()
|
||||
.route("/operations", get(list_operations).post(create_operation))
|
||||
.route("/operations/import", post(import_operation))
|
||||
.route("/operations/{operation_id}", get(get_operation))
|
||||
.route(
|
||||
"/operations/{operation_id}",
|
||||
get(get_operation)
|
||||
.patch(update_operation)
|
||||
.delete(delete_operation),
|
||||
)
|
||||
.route("/operations/{operation_id}/versions", post(create_version))
|
||||
.route(
|
||||
"/operations/{operation_id}/versions/{version}",
|
||||
@@ -40,6 +45,10 @@ pub fn build_app(state: AppState) -> Router {
|
||||
"/operations/{operation_id}/publish",
|
||||
post(publish_operation),
|
||||
)
|
||||
.route(
|
||||
"/operations/{operation_id}/archive",
|
||||
post(archive_operation),
|
||||
)
|
||||
.route("/operations/{operation_id}/test-runs", post(run_test))
|
||||
.route(
|
||||
"/operations/{operation_id}/samples/input-json",
|
||||
@@ -84,10 +93,7 @@ pub fn build_app(state: AppState) -> Router {
|
||||
"/invitations",
|
||||
get(list_invitations).post(create_invitation),
|
||||
)
|
||||
.route(
|
||||
"/invitations/{invitation_id}",
|
||||
axum::routing::delete(delete_invitation),
|
||||
)
|
||||
.route("/invitations/{invitation_id}", delete(delete_invitation))
|
||||
.route(
|
||||
"/platform-api-keys",
|
||||
get(list_platform_api_keys).post(create_platform_api_key),
|
||||
@@ -98,7 +104,7 @@ pub fn build_app(state: AppState) -> Router {
|
||||
)
|
||||
.route(
|
||||
"/platform-api-keys/{key_id}",
|
||||
axum::routing::delete(delete_platform_api_key),
|
||||
delete(delete_platform_api_key),
|
||||
)
|
||||
.route("/logs", get(list_logs))
|
||||
.route("/logs/{log_id}", get(get_log))
|
||||
@@ -213,6 +219,150 @@ mod tests {
|
||||
assert_eq!(test_run["response_preview"]["id"], "lead_123");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updates_archives_and_deletes_operation() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("operation_mutations");
|
||||
let upstream_base_url = spawn_upstream_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_operation_payload(
|
||||
&upstream_base_url,
|
||||
"crm_mutable_operation",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
|
||||
|
||||
let listed = client
|
||||
.get(format!("{base_url}/operations"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(listed["total"], 1);
|
||||
assert_eq!(listed["items"][0]["category"], "sales");
|
||||
|
||||
let updated = client
|
||||
.patch(format!("{base_url}/operations/{operation_id}"))
|
||||
.json(&json!({
|
||||
"display_name": "Create Lead Updated",
|
||||
"category": "marketing",
|
||||
"target": {
|
||||
"kind": "rest",
|
||||
"base_url": upstream_base_url,
|
||||
"method": "POST",
|
||||
"path_template": "/crm/leads",
|
||||
"static_headers": {}
|
||||
},
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"required": true,
|
||||
"nullable": false,
|
||||
"fields": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"required": true,
|
||||
"nullable": false,
|
||||
"fields": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"input_mapping": {
|
||||
"rules": [
|
||||
{
|
||||
"source": "$.mcp.email",
|
||||
"target": "$.request.body.email",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"output_mapping": {
|
||||
"rules": [
|
||||
{
|
||||
"source": "$.response.body.id",
|
||||
"target": "$.output.id",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"execution_config": {
|
||||
"timeout_ms": 1000,
|
||||
"headers": {}
|
||||
},
|
||||
"tool_description": {
|
||||
"title": "Create Lead Updated",
|
||||
"description": "Creates a CRM lead",
|
||||
"tags": ["crm"]
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated["status"], "draft");
|
||||
|
||||
let detail = client
|
||||
.get(format!("{base_url}/operations/{operation_id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(detail["display_name"], "Create Lead Updated");
|
||||
assert_eq!(detail["category"], "marketing");
|
||||
|
||||
let archived = client
|
||||
.post(format!("{base_url}/operations/{operation_id}/archive"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(archived["status"], "archived");
|
||||
|
||||
let deleted = client
|
||||
.delete(format!("{base_url}/operations/{operation_id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(deleted["operation_id"], operation_id);
|
||||
|
||||
let missing = client
|
||||
.get(format!("{base_url}/operations/{operation_id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(missing.status(), reqwest::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_binds_and_publishes_agent() {
|
||||
let registry = test_registry().await;
|
||||
@@ -984,6 +1134,7 @@ mod tests {
|
||||
OperationPayload {
|
||||
name: name.to_owned(),
|
||||
display_name: "Create Lead".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: base_url.to_owned(),
|
||||
@@ -1035,6 +1186,7 @@ mod tests {
|
||||
OperationPayload {
|
||||
name: name.to_owned(),
|
||||
display_name: "Create Lead GraphQL".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Graphql,
|
||||
target: Target::Graphql(GraphqlTarget {
|
||||
endpoint: endpoint.to_owned(),
|
||||
@@ -1089,6 +1241,7 @@ mod tests {
|
||||
OperationPayload {
|
||||
name: name.to_owned(),
|
||||
display_name: "Unary Echo gRPC".to_owned(),
|
||||
category: "support".to_owned(),
|
||||
protocol: Protocol::Grpc,
|
||||
target: Target::Grpc(GrpcTarget {
|
||||
server_addr: server_addr.to_owned(),
|
||||
|
||||
@@ -126,6 +126,11 @@ impl From<RegistryError> for ApiError {
|
||||
} => Self::not_found(format!(
|
||||
"operation version {version} for {operation_id} was not found"
|
||||
)),
|
||||
RegistryError::OperationHasPublishedAgentBindings { operation_id } => {
|
||||
Self::conflict(format!(
|
||||
"operation {operation_id} cannot be deleted while it is bound to a published agent"
|
||||
))
|
||||
}
|
||||
RegistryError::AuthProfileNotFound { auth_profile_id } => {
|
||||
Self::not_found(format!("auth profile {auth_profile_id} was not found"))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
error::ApiError,
|
||||
service::{
|
||||
ExportQuery, GenerateDraftPayload, ImportQuery, NewVersionPayload, OperationPayload,
|
||||
PublishPayload, TestRunPayload,
|
||||
PublishPayload, TestRunPayload, UpdateOperationPayload,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -43,7 +43,13 @@ pub async fn list_operations(
|
||||
.service
|
||||
.list_operations(&path.workspace_id.as_str().into())
|
||||
.await?;
|
||||
Ok(Json(json!({ "items": items })))
|
||||
let total = items.len();
|
||||
Ok(Json(json!({
|
||||
"items": items,
|
||||
"page": 1,
|
||||
"page_size": total,
|
||||
"total": total
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn create_operation(
|
||||
@@ -72,6 +78,36 @@ pub async fn get_operation(
|
||||
Ok(Json(json!(operation)))
|
||||
}
|
||||
|
||||
pub async fn update_operation(
|
||||
Path(path): Path<WorkspaceOperationPath>,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<UpdateOperationPayload>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let result = state
|
||||
.service
|
||||
.update_operation(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.operation_id.as_str().into(),
|
||||
payload,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(result)))
|
||||
}
|
||||
|
||||
pub async fn delete_operation(
|
||||
Path(path): Path<WorkspaceOperationPath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let result = state
|
||||
.service
|
||||
.delete_operation(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.operation_id.as_str().into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(result)))
|
||||
}
|
||||
|
||||
pub async fn get_operation_version(
|
||||
Path(path): Path<WorkspaceOperationVersionPath>,
|
||||
State(state): State<AppState>,
|
||||
@@ -119,6 +155,20 @@ pub async fn publish_operation(
|
||||
Ok(Json(json!(published)))
|
||||
}
|
||||
|
||||
pub async fn archive_operation(
|
||||
Path(path): Path<WorkspaceOperationPath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let archived = state
|
||||
.service
|
||||
.archive_operation(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.operation_id.as_str().into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(archived)))
|
||||
}
|
||||
|
||||
pub async fn run_test(
|
||||
Path(path): Path<WorkspaceOperationPath>,
|
||||
State(state): State<AppState>,
|
||||
|
||||
+336
-10
@@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
@@ -15,11 +16,12 @@ use crank_registry::{
|
||||
AgentSummary, AgentVersionRecord, CreateAgentRequest, CreateInvitationRequest,
|
||||
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateVersionRequest,
|
||||
CreateWorkspaceRequest, InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery,
|
||||
MembershipRecord, OperationSampleMetadata, OperationSummary, OperationVersionRecord,
|
||||
PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryOperation,
|
||||
SampleKind, SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket,
|
||||
UsageOperationBreakdown, UsageQuery, UsageSummary, UsageTimelinePoint, WorkspaceRecord,
|
||||
MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
|
||||
OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord, PostgresRegistry,
|
||||
PublishAgentRequest, PublishRequest, RegistryOperation, SampleKind, SaveAgentBindingsRequest,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery,
|
||||
UsageSummary, UsageTimelinePoint, WorkspaceRecord,
|
||||
};
|
||||
use crank_runtime::{PreparedRequest, RuntimeError, RuntimeExecutor, RuntimeOperation};
|
||||
use crank_schema::Schema;
|
||||
@@ -43,6 +45,8 @@ pub struct AdminService {
|
||||
pub struct OperationPayload {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
#[serde(default = "default_operation_category")]
|
||||
pub category: String,
|
||||
pub protocol: Protocol,
|
||||
pub target: Target,
|
||||
pub input_schema: Schema,
|
||||
@@ -240,6 +244,7 @@ pub struct CreatedOperationResponse {
|
||||
pub workspace_id: String,
|
||||
pub version: u32,
|
||||
pub status: OperationStatus,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -250,6 +255,86 @@ pub struct PublishResponse {
|
||||
pub published_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct UpdateOperationPayload {
|
||||
pub display_name: String,
|
||||
#[serde(default = "default_operation_category")]
|
||||
pub category: String,
|
||||
pub target: Target,
|
||||
pub input_schema: Schema,
|
||||
pub output_schema: Schema,
|
||||
pub input_mapping: MappingSet,
|
||||
pub output_mapping: MappingSet,
|
||||
pub execution_config: crank_core::ExecutionConfig,
|
||||
pub tool_description: crank_core::ToolDescription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OperationUsageSummaryView {
|
||||
pub calls_today: u64,
|
||||
pub error_rate_pct: f64,
|
||||
pub avg_latency_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OperationAgentRefView {
|
||||
pub agent_id: String,
|
||||
pub agent_slug: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OperationSummaryView {
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub category: String,
|
||||
pub protocol: Protocol,
|
||||
pub status: OperationStatus,
|
||||
pub current_draft_version: u32,
|
||||
pub latest_published_version: Option<u32>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
pub usage_summary: OperationUsageSummaryView,
|
||||
pub agent_refs: Vec<OperationAgentRefView>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OperationDetailView {
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub category: String,
|
||||
pub protocol: Protocol,
|
||||
pub status: OperationStatus,
|
||||
pub current_draft_version: u32,
|
||||
pub latest_published_version: Option<u32>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
pub draft_version_ref: VersionRef,
|
||||
pub published_version_ref: Option<VersionRef>,
|
||||
pub agent_refs: Vec<OperationAgentRefView>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct VersionRef {
|
||||
pub version: u32,
|
||||
pub status: OperationStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OperationMutationResult {
|
||||
pub operation_id: String,
|
||||
pub workspace_id: String,
|
||||
pub version: u32,
|
||||
pub status: OperationStatus,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ImportResponse {
|
||||
pub operation_id: String,
|
||||
@@ -280,6 +365,10 @@ pub struct GrpcMethodSummary {
|
||||
pub output_schema: Schema,
|
||||
}
|
||||
|
||||
fn default_operation_category() -> String {
|
||||
"general".to_owned()
|
||||
}
|
||||
|
||||
impl AdminService {
|
||||
pub fn new(registry: PostgresRegistry, storage_root: PathBuf) -> Self {
|
||||
Self {
|
||||
@@ -630,9 +719,37 @@ impl AdminService {
|
||||
pub async fn list_operations(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<OperationSummary>, ApiError> {
|
||||
) -> Result<Vec<OperationSummaryView>, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
Ok(self.registry.list_operations(workspace_id).await?)
|
||||
let summaries = self.registry.list_operations(workspace_id).await?;
|
||||
let usage = self
|
||||
.registry
|
||||
.list_operation_usage_summaries(workspace_id, &today_start_utc()?)
|
||||
.await?;
|
||||
let agent_refs = self
|
||||
.registry
|
||||
.list_operation_agent_refs(workspace_id)
|
||||
.await?;
|
||||
let usage_by_operation = usage_map(usage);
|
||||
let refs_by_operation = agent_ref_map(agent_refs);
|
||||
|
||||
Ok(summaries
|
||||
.into_iter()
|
||||
.map(|summary| {
|
||||
let operation_id = summary.id.as_str().to_owned();
|
||||
enrich_operation_summary(
|
||||
summary,
|
||||
usage_by_operation
|
||||
.get(&operation_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(default_usage_summary),
|
||||
refs_by_operation
|
||||
.get(&operation_id)
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
@@ -640,13 +757,45 @@ impl AdminService {
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
) -> Result<OperationSummary, ApiError> {
|
||||
self.registry
|
||||
) -> Result<OperationDetailView, ApiError> {
|
||||
let summary = self
|
||||
.registry
|
||||
.get_operation_summary(workspace_id, operation_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!("operation {} was not found", operation_id.as_str()))
|
||||
})
|
||||
})?;
|
||||
let agent_refs = self
|
||||
.registry
|
||||
.list_operation_agent_refs(workspace_id)
|
||||
.await?;
|
||||
let refs = agent_ref_map(agent_refs)
|
||||
.remove(operation_id.as_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(OperationDetailView {
|
||||
id: summary.id.as_str().to_owned(),
|
||||
workspace_id: summary.workspace_id.as_str().to_owned(),
|
||||
name: summary.name,
|
||||
display_name: summary.display_name,
|
||||
category: summary.category,
|
||||
protocol: summary.protocol,
|
||||
status: summary.status,
|
||||
current_draft_version: summary.current_draft_version,
|
||||
latest_published_version: summary.latest_published_version,
|
||||
created_at: summary.created_at,
|
||||
updated_at: summary.updated_at,
|
||||
published_at: summary.published_at,
|
||||
draft_version_ref: VersionRef {
|
||||
version: summary.current_draft_version,
|
||||
status: summary.status,
|
||||
},
|
||||
published_version_ref: summary.latest_published_version.map(|version| VersionRef {
|
||||
version,
|
||||
status: OperationStatus::Published,
|
||||
}),
|
||||
agent_refs: refs,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
@@ -693,6 +842,7 @@ impl AdminService {
|
||||
id: operation_id.clone(),
|
||||
name: payload.name,
|
||||
display_name: payload.display_name,
|
||||
category: payload.category,
|
||||
protocol: payload.protocol,
|
||||
status: OperationStatus::Draft,
|
||||
version: 1,
|
||||
@@ -724,6 +874,7 @@ impl AdminService {
|
||||
workspace_id: workspace_id.as_str().to_owned(),
|
||||
version: 1,
|
||||
status: OperationStatus::Draft,
|
||||
updated_at: snapshot.updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -743,6 +894,7 @@ impl AdminService {
|
||||
id: operation_id.clone(),
|
||||
name: payload.operation.name,
|
||||
display_name: payload.operation.display_name,
|
||||
category: payload.operation.category,
|
||||
protocol: payload.operation.protocol,
|
||||
status: OperationStatus::Draft,
|
||||
version,
|
||||
@@ -779,6 +931,62 @@ impl AdminService {
|
||||
workspace_id: workspace_id.as_str().to_owned(),
|
||||
version,
|
||||
status: OperationStatus::Draft,
|
||||
updated_at: snapshot.updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str()))]
|
||||
pub async fn update_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
payload: UpdateOperationPayload,
|
||||
) -> Result<OperationMutationResult, ApiError> {
|
||||
let existing = self
|
||||
.get_operation_version(
|
||||
workspace_id,
|
||||
operation_id,
|
||||
self.get_operation(workspace_id, operation_id)
|
||||
.await?
|
||||
.current_draft_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let updated_at = now_string()?;
|
||||
let snapshot = RegistryOperation {
|
||||
id: operation_id.clone(),
|
||||
name: existing.snapshot.name,
|
||||
display_name: payload.display_name,
|
||||
category: payload.category,
|
||||
protocol: existing.snapshot.protocol,
|
||||
status: OperationStatus::Draft,
|
||||
version: existing.version,
|
||||
target: payload.target,
|
||||
input_schema: payload.input_schema,
|
||||
output_schema: payload.output_schema,
|
||||
input_mapping: payload.input_mapping,
|
||||
output_mapping: payload.output_mapping,
|
||||
execution_config: payload.execution_config,
|
||||
tool_description: payload.tool_description,
|
||||
samples: existing.snapshot.samples,
|
||||
generated_draft: existing.snapshot.generated_draft,
|
||||
config_export: existing.snapshot.config_export,
|
||||
created_at: existing.snapshot.created_at,
|
||||
updated_at: updated_at.clone(),
|
||||
published_at: existing.snapshot.published_at,
|
||||
};
|
||||
|
||||
self.validate_registry_operation(&snapshot)?;
|
||||
self.registry
|
||||
.update_operation_draft(workspace_id, &snapshot)
|
||||
.await?;
|
||||
|
||||
Ok(OperationMutationResult {
|
||||
operation_id: operation_id.as_str().to_owned(),
|
||||
workspace_id: workspace_id.as_str().to_owned(),
|
||||
version: snapshot.version,
|
||||
status: snapshot.status,
|
||||
updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -809,6 +1017,48 @@ impl AdminService {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), fields(operation_id = %operation_id.as_str()))]
|
||||
pub async fn archive_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
) -> Result<OperationMutationResult, ApiError> {
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
let updated_at = now_string()?;
|
||||
self.registry
|
||||
.archive_operation(workspace_id, operation_id, &updated_at)
|
||||
.await?;
|
||||
|
||||
Ok(OperationMutationResult {
|
||||
operation_id: operation_id.as_str().to_owned(),
|
||||
workspace_id: workspace_id.as_str().to_owned(),
|
||||
version: summary.current_draft_version,
|
||||
status: OperationStatus::Archived,
|
||||
updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), fields(operation_id = %operation_id.as_str()))]
|
||||
pub async fn delete_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
) -> Result<OperationMutationResult, ApiError> {
|
||||
let summary = self.get_operation(workspace_id, operation_id).await?;
|
||||
let updated_at = now_string()?;
|
||||
self.registry
|
||||
.delete_operation(workspace_id, operation_id)
|
||||
.await?;
|
||||
|
||||
Ok(OperationMutationResult {
|
||||
operation_id: operation_id.as_str().to_owned(),
|
||||
workspace_id: workspace_id.as_str().to_owned(),
|
||||
version: summary.current_draft_version,
|
||||
status: summary.status,
|
||||
updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), version = payload.version))]
|
||||
pub async fn run_test(
|
||||
&self,
|
||||
@@ -1328,6 +1578,7 @@ impl AdminService {
|
||||
let payload = OperationPayload {
|
||||
name: document.operation.name.clone(),
|
||||
display_name: document.operation.display_name.clone(),
|
||||
category: document.operation.category.clone(),
|
||||
protocol: document.operation.protocol,
|
||||
target: document.operation.target.clone(),
|
||||
input_schema: document.operation.input_schema.clone(),
|
||||
@@ -1515,6 +1766,13 @@ impl AdminService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_registry_operation(&self, operation: &RegistryOperation) -> Result<(), ApiError> {
|
||||
validate_protocol_target(operation.protocol, &operation.target)?;
|
||||
operation.input_mapping.validate_paths()?;
|
||||
operation.output_mapping.validate_paths()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_operation_by_name(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
@@ -1755,6 +2013,74 @@ fn usage_window(period: UsagePeriod) -> Result<(UsagePeriod, String, UsageBucket
|
||||
))
|
||||
}
|
||||
|
||||
fn today_start_utc() -> Result<String, ApiError> {
|
||||
OffsetDateTime::now_utc()
|
||||
.replace_time(time::Time::MIDNIGHT)
|
||||
.format(&Rfc3339)
|
||||
.map_err(|error| ApiError::internal(error.to_string()))
|
||||
}
|
||||
|
||||
fn default_usage_summary() -> OperationUsageSummaryView {
|
||||
OperationUsageSummaryView {
|
||||
calls_today: 0,
|
||||
error_rate_pct: 0.0,
|
||||
avg_latency_ms: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn usage_map(items: Vec<OperationUsageSummary>) -> BTreeMap<String, OperationUsageSummaryView> {
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
(
|
||||
item.operation_id.as_str().to_owned(),
|
||||
OperationUsageSummaryView {
|
||||
calls_today: item.calls_today,
|
||||
error_rate_pct: item.error_rate_pct,
|
||||
avg_latency_ms: item.avg_latency_ms,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn agent_ref_map(items: Vec<OperationAgentRef>) -> BTreeMap<String, Vec<OperationAgentRefView>> {
|
||||
let mut map = BTreeMap::<String, Vec<OperationAgentRefView>>::new();
|
||||
for item in items {
|
||||
map.entry(item.operation_id.as_str().to_owned())
|
||||
.or_default()
|
||||
.push(OperationAgentRefView {
|
||||
agent_id: item.agent_id.as_str().to_owned(),
|
||||
agent_slug: item.agent_slug,
|
||||
display_name: item.display_name,
|
||||
});
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
fn enrich_operation_summary(
|
||||
summary: OperationSummary,
|
||||
usage_summary: OperationUsageSummaryView,
|
||||
agent_refs: Vec<OperationAgentRefView>,
|
||||
) -> OperationSummaryView {
|
||||
OperationSummaryView {
|
||||
id: summary.id.as_str().to_owned(),
|
||||
workspace_id: summary.workspace_id.as_str().to_owned(),
|
||||
name: summary.name,
|
||||
display_name: summary.display_name,
|
||||
category: summary.category,
|
||||
protocol: summary.protocol,
|
||||
status: summary.status,
|
||||
current_draft_version: summary.current_draft_version,
|
||||
latest_published_version: summary.latest_published_version,
|
||||
created_at: summary.created_at,
|
||||
updated_at: summary.updated_at,
|
||||
published_at: summary.published_at,
|
||||
usage_summary,
|
||||
agent_refs,
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_service_summary(service: ProtoService) -> Result<GrpcServiceSummary, ApiError> {
|
||||
let methods = service
|
||||
.unary_methods()
|
||||
|
||||
Reference in New Issue
Block a user