feat: add workspace foundation

This commit is contained in:
a.tolmachev
2026-03-29 21:45:53 +03:00
parent d3ab565fff
commit d757adb192
19 changed files with 743 additions and 109 deletions
+16 -3
View File
@@ -12,12 +12,13 @@ use crate::{
run_test, upload_descriptor_set, upload_input_json, upload_output_json,
upload_proto_descriptor,
},
workspaces::{create_workspace, get_workspace, list_workspaces, update_workspace},
},
state::AppState,
};
pub fn build_app(state: AppState) -> Router {
let admin_router = Router::new()
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))
@@ -62,9 +63,16 @@ pub fn build_app(state: AppState) -> Router {
)
.route("/auth-profiles/{auth_profile_id}", get(get_auth_profile));
let admin_router = Router::new()
.route("/workspaces", get(list_workspaces).post(create_workspace))
.route(
"/workspaces/{workspace_id}",
get(get_workspace).patch(update_workspace),
)
.nest("/workspaces/{workspace_id}", workspace_router);
Router::new()
.route("/health", get(crate::routes::health))
.merge(admin_router.clone())
.nest("/api/admin", admin_router)
.with_state(state)
}
@@ -98,6 +106,8 @@ mod tests {
state::AppState,
};
const DEFAULT_WORKSPACE_ID: &str = "ws_default";
#[tokio::test]
async fn creates_publishes_and_tests_rest_operation() {
let registry = test_registry().await;
@@ -590,7 +600,10 @@ mod tests {
axum::serve(listener, app).await.unwrap();
});
format!("http://{}", address)
format!(
"http://{}/api/admin/workspaces/{}",
address, DEFAULT_WORKSPACE_ID
)
}
async fn create_lead(Json(payload): Json<Value>) -> Json<Value> {
+6
View File
@@ -96,6 +96,9 @@ impl IntoResponse for ApiError {
impl From<RegistryError> for ApiError {
fn from(value: RegistryError) -> Self {
match value {
RegistryError::WorkspaceNotFound { workspace_id } => {
Self::not_found(format!("workspace {workspace_id} was not found"))
}
RegistryError::OperationNotFound { operation_id } => {
Self::not_found(format!("operation {operation_id} was not found"))
}
@@ -111,6 +114,9 @@ impl From<RegistryError> for ApiError {
RegistryError::OperationAlreadyExists { operation_id } => {
Self::conflict(format!("operation {operation_id} already exists"))
}
RegistryError::WorkspaceSlugAlreadyExists { slug } => {
Self::conflict(format!("workspace with slug {slug} already exists"))
}
RegistryError::InvalidInitialVersion { .. }
| RegistryError::InvalidVersionSequence { .. }
| RegistryError::ImmutableOperationFieldChanged { .. }
+1
View File
@@ -1,5 +1,6 @@
pub mod auth_profiles;
pub mod operations;
pub mod workspaces;
use axum::Json;
use serde_json::json;
+30 -5
View File
@@ -2,30 +2,55 @@ use axum::{
Json,
extract::{Path, State},
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{error::ApiError, service::AuthProfilePayload, state::AppState};
pub async fn list_auth_profiles(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
let items = state.service.list_auth_profiles().await?;
#[derive(Deserialize)]
pub struct WorkspacePath {
pub workspace_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceAuthProfilePath {
pub workspace_id: String,
pub auth_profile_id: String,
}
pub async fn list_auth_profiles(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_auth_profiles(&path.workspace_id.as_str().into())
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_auth_profile(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Json(payload): Json<AuthProfilePayload>,
) -> Result<Json<Value>, ApiError> {
let profile = state.service.create_auth_profile(payload).await?;
let profile = state
.service
.create_auth_profile(&path.workspace_id.as_str().into(), payload)
.await?;
Ok(Json(json!(profile)))
}
pub async fn get_auth_profile(
Path(auth_profile_id): Path<String>,
Path(path): Path<WorkspaceAuthProfilePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let profile = state
.service
.get_auth_profile(&auth_profile_id.as_str().into())
.get_auth_profile(
&path.workspace_id.as_str().into(),
&path.auth_profile_id.as_str().into(),
)
.await?;
Ok(Json(json!(profile)))
}
+96 -28
View File
@@ -5,6 +5,7 @@ use axum::{
http::{HeaderMap, StatusCode, header},
response::IntoResponse,
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{
@@ -16,86 +17,134 @@ use crate::{
state::AppState,
};
pub async fn list_operations(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
let items = state.service.list_operations().await?;
#[derive(Deserialize)]
pub struct WorkspacePath {
pub workspace_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceOperationPath {
pub workspace_id: String,
pub operation_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceOperationVersionPath {
pub workspace_id: String,
pub operation_id: String,
pub version: u32,
}
pub async fn list_operations(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_operations(&path.workspace_id.as_str().into())
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_operation(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Json(payload): Json<OperationPayload>,
) -> Result<Json<Value>, ApiError> {
let created = state.service.create_operation(payload).await?;
let created = state
.service
.create_operation(&path.workspace_id.as_str().into(), payload)
.await?;
Ok(Json(json!(created)))
}
pub async fn get_operation(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let operation = state
.service
.get_operation(&operation_id.as_str().into())
.get_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
)
.await?;
Ok(Json(json!(operation)))
}
pub async fn get_operation_version(
Path((operation_id, version)): Path<(String, u32)>,
Path(path): Path<WorkspaceOperationVersionPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let version = state
.service
.get_operation_version(&operation_id.as_str().into(), version)
.get_operation_version(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
path.version,
)
.await?;
Ok(Json(json!(version)))
}
pub async fn create_version(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<NewVersionPayload>,
) -> Result<Json<Value>, ApiError> {
let created = state
.service
.create_version(&operation_id.as_str().into(), payload)
.create_version(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(created)))
}
pub async fn publish_operation(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<PublishPayload>,
) -> Result<Json<Value>, ApiError> {
let published = state
.service
.publish_operation(&operation_id.as_str().into(), payload.version)
.publish_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload.version,
)
.await?;
Ok(Json(json!(published)))
}
pub async fn run_test(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<TestRunPayload>,
) -> Result<Json<Value>, ApiError> {
let result = state
.service
.run_test(&operation_id.as_str().into(), payload)
.run_test(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(result)))
}
pub async fn upload_input_json(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<Value>,
) -> Result<Json<Value>, ApiError> {
let sample = state
.service
.save_json_sample(
&operation_id.as_str().into(),
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
crank_registry::SampleKind::InputJson,
&payload,
)
@@ -109,14 +158,15 @@ pub async fn upload_input_json(
}
pub async fn upload_output_json(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<Value>,
) -> Result<Json<Value>, ApiError> {
let sample = state
.service
.save_json_sample(
&operation_id.as_str().into(),
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
crank_registry::SampleKind::OutputJson,
&payload,
)
@@ -130,7 +180,7 @@ pub async fn upload_output_json(
}
pub async fn upload_proto_descriptor(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
@@ -139,7 +189,8 @@ pub async fn upload_proto_descriptor(
let descriptor = state
.service
.upload_proto_file(
&operation_id.as_str().into(),
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
source_name.as_deref(),
body.as_ref(),
)
@@ -149,7 +200,7 @@ pub async fn upload_proto_descriptor(
}
pub async fn upload_descriptor_set(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
@@ -158,7 +209,8 @@ pub async fn upload_descriptor_set(
let descriptor = state
.service
.upload_descriptor_set(
&operation_id.as_str().into(),
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
source_name.as_deref(),
body.as_ref(),
)
@@ -168,37 +220,49 @@ pub async fn upload_descriptor_set(
}
pub async fn list_grpc_services(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
Query(query): Query<ExportQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let services = state
.service
.list_grpc_services(&operation_id.as_str().into(), query.version)
.list_grpc_services(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
query.version,
)
.await?;
Ok(Json(json!({ "services": services })))
}
pub async fn generate_draft(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<GenerateDraftPayload>,
) -> Result<Json<Value>, ApiError> {
let draft = state
.service
.generate_draft(&operation_id.as_str().into(), payload)
.generate_draft(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(draft)))
}
pub async fn export_operation(
Path(operation_id): Path<String>,
Path(path): Path<WorkspaceOperationPath>,
Query(query): Query<ExportQuery>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, ApiError> {
let yaml = state
.service
.export_operation(&operation_id.as_str().into(), query)
.export_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
query,
)
.await?;
let mut headers = HeaderMap::new();
headers.insert(
@@ -210,11 +274,15 @@ pub async fn export_operation(
}
pub async fn import_operation(
Path(path): Path<WorkspacePath>,
Query(query): Query<ImportQuery>,
State(state): State<AppState>,
body: String,
) -> Result<Json<Value>, ApiError> {
let imported = state.service.import_operation(query, &body).await?;
let imported = state
.service
.import_operation(&path.workspace_id.as_str().into(), query, &body)
.await?;
Ok(Json(json!(imported)))
}
+47
View File
@@ -0,0 +1,47 @@
use axum::{
Json,
extract::{Path, State},
};
use serde_json::{Value, json};
use crate::{
error::ApiError,
service::{UpdateWorkspacePayload, WorkspacePayload},
state::AppState,
};
pub async fn list_workspaces(State(state): State<AppState>) -> Result<Json<Value>, ApiError> {
let items = state.service.list_workspaces().await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_workspace(
State(state): State<AppState>,
Json(payload): Json<WorkspacePayload>,
) -> Result<Json<Value>, ApiError> {
let workspace = state.service.create_workspace(payload).await?;
Ok(Json(json!(workspace)))
}
pub async fn get_workspace(
Path(workspace_id): Path<String>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let workspace = state
.service
.get_workspace(&workspace_id.as_str().into())
.await?;
Ok(Json(json!(workspace)))
}
pub async fn update_workspace(
Path(workspace_id): Path<String>,
State(state): State<AppState>,
Json(payload): Json<UpdateWorkspacePayload>,
) -> Result<Json<Value>, ApiError> {
let workspace = state
.service
.update_workspace(&workspace_id.as_str().into(), payload)
.await?;
Ok(Json(json!(workspace)))
}
+165 -26
View File
@@ -3,13 +3,15 @@ use std::path::PathBuf;
use crank_core::{
AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport, ExportMode, GeneratedDraft,
GeneratedDraftStatus, OperationId, OperationStatus, Protocol, SampleId, Samples, Target,
Workspace, WorkspaceId, WorkspaceStatus,
};
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
use crank_proto::{ProtoService, services_from_descriptor_set_bytes};
use crank_registry::{
CreateVersionRequest, OperationSampleMetadata, OperationSummary, OperationVersionRecord,
PostgresRegistry, PublishRequest, RegistryOperation, SampleKind, SaveAuthProfileRequest,
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
CreateVersionRequest, CreateWorkspaceRequest, OperationSampleMetadata, OperationSummary,
OperationVersionRecord, PostgresRegistry, PublishRequest, RegistryOperation, SampleKind,
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
UpdateWorkspaceRequest, WorkspaceRecord,
};
use crank_runtime::{PreparedRequest, RuntimeError, RuntimeExecutor, RuntimeOperation};
use crank_schema::Schema;
@@ -76,6 +78,22 @@ pub struct AuthProfilePayload {
pub config: AuthConfig,
}
#[derive(Clone, Debug, Deserialize)]
pub struct WorkspacePayload {
pub slug: String,
pub display_name: String,
#[serde(default)]
pub settings: Value,
}
#[derive(Clone, Debug, Deserialize)]
pub struct UpdateWorkspacePayload {
pub slug: Option<String>,
pub display_name: Option<String>,
pub status: Option<WorkspaceStatus>,
pub settings: Option<Value>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct GenerateDraftPayload {
#[serde(default)]
@@ -122,6 +140,7 @@ pub struct YamlOperationDocument {
#[derive(Clone, Debug, Serialize)]
pub struct CreatedOperationResponse {
pub operation_id: String,
pub workspace_id: String,
pub version: u32,
pub status: OperationStatus,
}
@@ -129,6 +148,7 @@ pub struct CreatedOperationResponse {
#[derive(Clone, Debug, Serialize)]
pub struct PublishResponse {
pub operation_id: String,
pub workspace_id: String,
pub published_version: u32,
pub published_at: String,
}
@@ -136,6 +156,7 @@ pub struct PublishResponse {
#[derive(Clone, Debug, Serialize)]
pub struct ImportResponse {
pub operation_id: String,
pub workspace_id: String,
pub version: u32,
pub import_mode: ImportMode,
pub warnings: Vec<String>,
@@ -172,17 +193,89 @@ impl AdminService {
}
#[instrument(skip(self))]
pub async fn list_operations(&self) -> Result<Vec<OperationSummary>, ApiError> {
Ok(self.registry.list_operations().await?)
pub async fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>, ApiError> {
Ok(self.registry.list_workspaces().await?)
}
#[instrument(skip(self, payload), fields(workspace_slug = %payload.slug))]
pub async fn create_workspace(
&self,
payload: WorkspacePayload,
) -> Result<WorkspaceRecord, ApiError> {
let now = now_string()?;
let workspace = Workspace {
id: WorkspaceId::new(new_prefixed_id("ws")),
slug: payload.slug,
display_name: payload.display_name,
status: WorkspaceStatus::Active,
settings: payload.settings,
created_at: now.clone(),
updated_at: now,
};
self.registry
.create_workspace(CreateWorkspaceRequest {
workspace: &workspace,
})
.await?;
Ok(WorkspaceRecord { workspace })
}
pub async fn get_workspace(
&self,
workspace_id: &WorkspaceId,
) -> Result<WorkspaceRecord, ApiError> {
self.registry
.get_workspace(workspace_id)
.await?
.ok_or_else(|| {
ApiError::not_found(format!("workspace {} was not found", workspace_id.as_str()))
})
}
#[instrument(skip(self, payload), fields(workspace_id = %workspace_id.as_str()))]
pub async fn update_workspace(
&self,
workspace_id: &WorkspaceId,
payload: UpdateWorkspacePayload,
) -> Result<WorkspaceRecord, ApiError> {
let existing = self.get_workspace(workspace_id).await?.workspace;
let workspace = Workspace {
id: existing.id,
slug: payload.slug.unwrap_or(existing.slug),
display_name: payload.display_name.unwrap_or(existing.display_name),
status: payload.status.unwrap_or(existing.status),
settings: payload.settings.unwrap_or(existing.settings),
created_at: existing.created_at,
updated_at: now_string()?,
};
self.registry
.update_workspace(UpdateWorkspaceRequest {
workspace: &workspace,
})
.await?;
Ok(WorkspaceRecord { workspace })
}
pub async fn list_operations(
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<OperationSummary>, ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
Ok(self.registry.list_operations(workspace_id).await?)
}
#[instrument(skip(self))]
pub async fn get_operation(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
) -> Result<OperationSummary, ApiError> {
self.registry
.get_operation_summary(operation_id)
.get_operation_summary(workspace_id, operation_id)
.await?
.ok_or_else(|| {
ApiError::not_found(format!("operation {} was not found", operation_id.as_str()))
@@ -192,11 +285,12 @@ impl AdminService {
#[instrument(skip(self))]
pub async fn get_operation_version(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
version: u32,
) -> Result<OperationVersionRecord, ApiError> {
self.registry
.get_operation_version(operation_id, version)
.get_operation_version(workspace_id, operation_id, version)
.await?
.ok_or_else(|| {
ApiError::not_found(format!(
@@ -209,11 +303,17 @@ impl AdminService {
#[instrument(skip(self, payload), fields(protocol = ?payload.protocol, operation_name = %payload.name))]
pub async fn create_operation(
&self,
workspace_id: &WorkspaceId,
payload: OperationPayload,
) -> Result<CreatedOperationResponse, ApiError> {
self.validate_operation_payload(&payload)?;
self.ensure_workspace_exists(workspace_id).await?;
if self.find_operation_by_name(&payload.name).await?.is_some() {
if self
.find_operation_by_name(workspace_id, &payload.name)
.await?
.is_some()
{
return Err(ApiError::conflict(format!(
"operation with name {} already exists",
payload.name
@@ -247,11 +347,14 @@ impl AdminService {
published_at: None,
};
self.registry.create_operation(&snapshot, None).await?;
self.registry
.create_operation(workspace_id, &snapshot, None)
.await?;
info!(operation_id = %operation_id.as_str(), version = 1, "operation created");
Ok(CreatedOperationResponse {
operation_id: operation_id.as_str().to_owned(),
workspace_id: workspace_id.as_str().to_owned(),
version: 1,
status: OperationStatus::Draft,
})
@@ -260,12 +363,13 @@ impl AdminService {
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), protocol = ?payload.operation.protocol))]
pub async fn create_version(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
payload: NewVersionPayload,
) -> Result<CreatedOperationResponse, ApiError> {
self.validate_operation_payload(&payload.operation)?;
let summary = self.get_operation(operation_id).await?;
let summary = self.get_operation(workspace_id, operation_id).await?;
let now = now_string()?;
let version = summary.current_draft_version + 1;
let snapshot = RegistryOperation {
@@ -295,6 +399,7 @@ impl AdminService {
self.registry
.create_version(CreateVersionRequest {
workspace_id,
snapshot: &snapshot,
change_note: payload.change_note.as_deref(),
created_by: None,
@@ -304,6 +409,7 @@ impl AdminService {
Ok(CreatedOperationResponse {
operation_id: operation_id.as_str().to_owned(),
workspace_id: workspace_id.as_str().to_owned(),
version,
status: OperationStatus::Draft,
})
@@ -312,12 +418,14 @@ impl AdminService {
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version))]
pub async fn publish_operation(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
version: u32,
) -> Result<PublishResponse, ApiError> {
let published_at = now_string()?;
self.registry
.publish_operation(PublishRequest {
workspace_id,
operation_id,
version,
published_at: &published_at,
@@ -328,6 +436,7 @@ impl AdminService {
Ok(PublishResponse {
operation_id: operation_id.as_str().to_owned(),
workspace_id: workspace_id.as_str().to_owned(),
published_version: version,
published_at,
})
@@ -336,11 +445,12 @@ impl AdminService {
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), version = payload.version))]
pub async fn run_test(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
payload: TestRunPayload,
) -> Result<TestRunResult, ApiError> {
let record = self
.get_operation_version(operation_id, payload.version)
.get_operation_version(workspace_id, operation_id, payload.version)
.await?;
let runtime = RuntimeOperation::from(record.snapshot.clone());
let request_preview =
@@ -375,18 +485,23 @@ impl AdminService {
}
#[instrument(skip(self))]
pub async fn list_auth_profiles(&self) -> Result<Vec<AuthProfile>, ApiError> {
Ok(self.registry.list_auth_profiles().await?)
pub async fn list_auth_profiles(
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<AuthProfile>, ApiError> {
self.ensure_workspace_exists(workspace_id).await?;
Ok(self.registry.list_auth_profiles(workspace_id).await?)
}
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("descriptor-set.bin")))]
pub async fn upload_descriptor_set(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
source_name: Option<&str>,
payload: &[u8],
) -> Result<DescriptorUploadResponse, ApiError> {
let summary = self.get_operation(operation_id).await?;
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Grpc {
return Err(ApiError::validation(
"descriptor upload is only allowed for grpc operations",
@@ -441,11 +556,12 @@ impl AdminService {
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("schema.proto")))]
pub async fn upload_proto_file(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
source_name: Option<&str>,
payload: &[u8],
) -> Result<DescriptorUploadResponse, ApiError> {
let summary = self.get_operation(operation_id).await?;
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Grpc {
return Err(ApiError::validation(
"proto upload is only allowed for grpc operations",
@@ -495,10 +611,11 @@ impl AdminService {
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version))]
pub async fn list_grpc_services(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
version: Option<u32>,
) -> Result<Vec<GrpcServiceSummary>, ApiError> {
let summary = self.get_operation(operation_id).await?;
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Grpc {
return Err(ApiError::validation(
"grpc services are only available for grpc operations",
@@ -529,10 +646,11 @@ impl AdminService {
#[instrument(skip(self))]
pub async fn get_auth_profile(
&self,
workspace_id: &WorkspaceId,
auth_profile_id: &AuthProfileId,
) -> Result<AuthProfile, ApiError> {
self.registry
.get_auth_profile(auth_profile_id)
.get_auth_profile(workspace_id, auth_profile_id)
.await?
.ok_or_else(|| {
ApiError::not_found(format!(
@@ -545,13 +663,16 @@ impl AdminService {
#[instrument(skip(self, payload), fields(auth_profile_name = %payload.name, auth_kind = ?payload.kind))]
pub async fn create_auth_profile(
&self,
workspace_id: &WorkspaceId,
payload: AuthProfilePayload,
) -> Result<AuthProfile, ApiError> {
validate_auth_profile_kind(payload.kind, &payload.config)?;
self.ensure_workspace_exists(workspace_id).await?;
let now = now_string()?;
let profile = AuthProfile {
id: AuthProfileId::new(new_prefixed_id("auth")),
workspace_id: workspace_id.clone(),
name: payload.name,
kind: payload.kind,
config: payload.config,
@@ -560,7 +681,10 @@ impl AdminService {
};
self.registry
.save_auth_profile(SaveAuthProfileRequest { profile: &profile })
.save_auth_profile(SaveAuthProfileRequest {
workspace_id,
profile: &profile,
})
.await?;
info!(auth_profile_id = %profile.id.as_str(), "auth profile created");
@@ -570,18 +694,21 @@ impl AdminService {
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version = query.version.unwrap_or_default(), mode = ?query.mode))]
pub async fn export_operation(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
query: ExportQuery,
) -> Result<String, ApiError> {
let version = match query.version {
Some(version) => version,
None => {
self.get_operation(operation_id)
self.get_operation(workspace_id, operation_id)
.await?
.current_draft_version
}
};
let record = self.get_operation_version(operation_id, version).await?;
let record = self
.get_operation_version(workspace_id, operation_id, version)
.await?;
let document = YamlOperationDocument {
format_version: "1".to_owned(),
kind: "operation".to_owned(),
@@ -600,6 +727,7 @@ impl AdminService {
#[instrument(skip(self, yaml_document), fields(mode = ?query.mode))]
pub async fn import_operation(
&self,
workspace_id: &WorkspaceId,
query: ImportQuery,
yaml_document: &str,
) -> Result<ImportResponse, ApiError> {
@@ -624,9 +752,10 @@ impl AdminService {
match query.mode {
ImportMode::Create => {
let created = self.create_operation(payload).await?;
let created = self.create_operation(workspace_id, payload).await?;
Ok(ImportResponse {
operation_id: created.operation_id,
workspace_id: created.workspace_id,
version: created.version,
import_mode: ImportMode::Create,
warnings: Vec::new(),
@@ -634,11 +763,12 @@ impl AdminService {
}
ImportMode::Upsert => {
if let Some(existing) = self
.find_operation_by_name(&document.operation.name)
.find_operation_by_name(workspace_id, &document.operation.name)
.await?
{
let created = self
.create_version(
workspace_id,
&existing.id,
NewVersionPayload {
operation: payload,
@@ -649,6 +779,7 @@ impl AdminService {
let response = ImportResponse {
operation_id: created.operation_id,
workspace_id: created.workspace_id,
version: created.version,
import_mode: ImportMode::Upsert,
warnings: Vec::new(),
@@ -660,9 +791,10 @@ impl AdminService {
);
Ok(response)
} else {
let created = self.create_operation(payload).await?;
let created = self.create_operation(workspace_id, payload).await?;
let response = ImportResponse {
operation_id: created.operation_id,
workspace_id: created.workspace_id,
version: created.version,
import_mode: ImportMode::Upsert,
warnings: Vec::new(),
@@ -681,11 +813,12 @@ impl AdminService {
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), sample_kind = ?sample_kind))]
pub async fn save_json_sample(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
sample_kind: SampleKind,
payload: &Value,
) -> Result<OperationSampleMetadata, ApiError> {
let summary = self.get_operation(operation_id).await?;
let summary = self.get_operation(workspace_id, operation_id).await?;
let version = summary.current_draft_version;
let sample_id = SampleId::new(new_prefixed_id("sample"));
let now = now_string()?;
@@ -725,10 +858,11 @@ impl AdminService {
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str()))]
pub async fn generate_draft(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
payload: GenerateDraftPayload,
) -> Result<DraftGenerationResult, ApiError> {
let summary = self.get_operation(operation_id).await?;
let summary = self.get_operation(workspace_id, operation_id).await?;
let samples = self
.registry
.list_sample_metadata(operation_id, summary.current_draft_version)
@@ -795,15 +929,20 @@ impl AdminService {
async fn find_operation_by_name(
&self,
workspace_id: &WorkspaceId,
name: &str,
) -> Result<Option<OperationSummary>, ApiError> {
Ok(self
.registry
.list_operations()
.list_operations(workspace_id)
.await?
.into_iter()
.find(|operation| operation.name == name))
}
async fn ensure_workspace_exists(&self, workspace_id: &WorkspaceId) -> Result<(), ApiError> {
self.get_workspace(workspace_id).await.map(|_| ())
}
}
fn build_request_preview(